schenke-io / livewire-auto-form
Enhanced livewire component to edit models and its relationships
Installs: 25
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/schenke-io/livewire-auto-form
Requires
- php: ^8.3
- archtechx/enums: ^v1.1
- illuminate/contracts: ^12.0
- illuminate/http: ^12.0
- illuminate/routing: ^12.0
- illuminate/support: ^12.0
- illuminate/validation: ^12.0
- livewire/flux: ^2.1
- livewire/livewire: ^4.0
- nette/neon: ^3.4
- spatie/laravel-data: ^4.0
- spatie/laravel-package-tools: ^1.0
Requires (Dev)
- ext-pcntl: *
- barryvdh/laravel-ide-helper: ^3.5
- larastan/larastan: ^3.5
- laravel/pint: ^1.24
- orchestra/testbench: ^10.2
- pestphp/pest: ^4.2
- pestphp/pest-plugin-browser: ^4.0
- phpstan/phpstan-phpunit: ^2.0
- phpunit/phpunit: ^12.3
- schenke-io/laravel-invoice: ^0.3.5
- schenke-io/laravel-relation-manager: ^2.0
- schenke-io/packaging-tools: ^0.2
- spatie/laravel-ray: ^1.40
This package is auto-updated.
Last update: 2026-02-23 16:40:41 UTC
README
Livewire Auto Form
Enhanced livewire component to edit models and its relationships
Stop manually mapping every Eloquent attribute to a Livewire property and start focusing on your app's core logic with our buffer-based form management.
If you struggle with the following problems, we are just for you:
- Tedious property definitions: Tired of manually adding
public string $namefor every model attribute? Our single-buffer architecture handles it all. - "Forgot-to-save" bugs: Eliminate accidental data loss with centralized state management and predictable auto-save logic.
- Relationship boilerplate: Editing child models shouldn't be hard. Handle relationships with simple method calls and zero extra code.
- Manual option lists: Stop hardcoding select options. Use
AutoFormOptionsto centralize labels in your Models and Enums, or rely on our smart automatic generation. - Rigid workflows: Switch between real-time "auto-save" and traditional "Save" buttons effortlessly, without rewriting your component.
- Complex Wizard Workflows: Building multi-step forms usually requires manual state management for steps. Our Wizard support automates navigation and validation.
- Complex testing: Logic consistency means fewer edge cases and easier unit testing for your form components.
Concept of Coding
Livewire Auto Form follows a buffer-based state management pattern. Instead of binding Livewire properties directly to Eloquent model attributes, it uses an internal $form object (an instance of FormCollection) to safely stage changes.
Core Principles
- State Isolation: All form data resides in a single
$formbuffer. This prevents accidental model mutations and allows for easy "undo" or "cancel" operations. Since$formis aFormCollection(implementingArrayAccess,Countable,IteratorAggregate, andWireable), it provides rich state management beyond a simple array. - Convention over Configuration: By extending
AutoFormand callingsetModel($model), the package manages field hydration and state transitions. Relationships and validation rules are defined in the component to maintain full control. - Context Switching: Swap the active model within the same component seamlessly. You can move between the root model and its relations, or even switch between different instances of the same model type (the "List & Edit" pattern). The package manages the state transition and buffer hydration automatically.
- Automatic Persistence: Choose between real-time updates (
autoSave = true) or manual submission. The package handles Eloquentsave()calls and validation. - Automatic Data Flattening: When loading data from models, attributes implementing
Livewire\Wireable,Stringable, or PHP Enums are automatically flattened to their scalar/string representation when possible. This ensures consistent data handling in the front-end and avoids serialization issues. - Standardized Options: Use the
AutoFormOptionsinterface to centralize option generation for selects and radios, with an automatic fallback for quick setups. - Multi-Step Workflows: Use
AutoWizardFormto split large forms into sequential steps with per-step validation and explicit field mapping.
This approach ensures that your components remain clean, predictable, and easy to test.
Rule Inheritance & Value Normalization
AutoForm can inherit validation rules from your Eloquent model and normalize complex value objects in a predictable way.
-
Rule Inheritance: If your model exposes a
rules(): arraymethod, the trait will inherit those rules based on the keys you provide inruleKeys(). You can also override inherited rules directly in therules()method by passing them as a second argument toscanInheritedRules().Example:
// In your model public function rules(): array { return [ 'name' => 'required|string', 'email' => 'email', 'internal_code' => 'string', ]; } // In your component public function ruleKeys(): array { return [ 'name', 'email', ]; } public function rules(): array { return $this->scanInheritedRules($this->ruleKeys(), [ 'name' => 'sometimes', // overrides model rule ]); }
-
Value Normalization: The internal
DataProcessorhandles special value objects:- Enums are flattened to their scalar representation.
Stringableobjects are converted to strings.Wireableobjects that do not return an array are flattened viatoLivewire().
Installation
composer require schenke-io/livewire-auto-form
- Livewire Auto Form
- Concept of Coding
- Code Examples
- Multi-Step Wizards
- API Definitions
- Persistence & Relationship Discovery
- Event System
Code Examples
This guide provides examples for using the package, ranging from basic forms to more advanced scenarios.
Note on Method Calls: Since the form logic is integrated directly into the component, you can call methods like
save()oredit()directly in Blade. This ensures full compatibility with Alpine.js and component libraries like Flux without the need for wrapper methods. See API Definitions for details.
1. The Basic Form (Manual Save)
Using scanInheritedRules with ruleKeys() to avoid duplication
You can let your models define rules() and inherit them in your component to keep validation logic in one place. Use ruleKeys() to declare which fields to inherit and pass overrides to scanInheritedRules() when needed.
use SchenkeIo\LivewireAutoForm\AutoForm; class EditPost extends AutoForm { public function ruleKeys(): array { // Will fetch `name` and `author.email` rules from the active model return ['name', 'author.email']; } public function rules(): array { return $this->scanInheritedRules( $this->ruleKeys(), ['author.email' => 'nullable|email'] // component override takes precedence ); } }
This is the simplest way to use the package. You extend AutoForm in your component and initialize it.
The Livewire Component:
use SchenkeIo\LivewireAutoForm\AutoForm; class EditPost extends AutoForm { public function mount(Post $post) { $this->setModel($post); } public function rules(): array { return [ 'title' => 'required|string|min:3', 'content' => 'required', ]; } public function render() { return view('livewire.edit-post'); } }
The Blade View:
<div> <input type="text" wire:model="form.title"> @error('form.title') <span class="error">{{ $message }}</span> @enderror <textarea wire:model="form.content"></textarea> @error('form.content') <span class="error">{{ $message }}</span> @enderror <button wire:click="save">Save Post</button> </div>
2. Modern "Auto-Save" Experience
If you want your form to save automatically as the user types (on blur), just set $this->autoSave to true.
The Livewire Component:
class EditPost extends AutoForm { public function mount(Post $post) { $this->setModel($post); $this->autoSave = true; } public function rules(): array { return [ 'title' => 'required|string|min:3', 'content' => 'required', ]; } }
The Blade View:
<div> <!-- No "Save" button needed! It saves when you click away from the input (on blur) --> <input type="text" wire:model.blur="form.title"> @error('form.title') <span class="error">{{ $message }}</span> @enderror <textarea wire:model.blur="form.content"></textarea> @error('form.content') <span class="error">{{ $message }}</span> @enderror <span wire:loading wire:target="save">Saving...</span> </div>
3. Handling Relationships
This is where the package really shines. Imagine a Brand that has many Products. You can edit the brand and its products in the same component.
The Livewire Component:
class EditBrand extends AutoForm { public function mount(Brand $brand) { $this->setModel($brand); } public function rules(): array { return [ 'name' => 'required', 'products.name' => 'required', 'products.price' => 'numeric', ]; } public function render() { return view('livewire.edit-brand'); } }
The Blade View:
<div> <!-- Main Brand Form --> <input type="text" wire:model.blur="form.name"> <h3>Products</h3> <ul> @foreach($this->getRelationList('products') as $product) <li wire:key="product-{{ $product->id }}" class="{{ $this->isEdited('products', $product->id) ? 'active' : '' }}"> {{ $product->name }} - ${{ $product->price }} <button wire:click="edit('products', {{ $product->id }})">Edit</button> <button wire:click="delete('products', {{ $product->id }})">Delete</button> </li> @endforeach </ul> <button wire:click="add('products')">Add Product</button> <!-- This shows up only when we are editing or adding a product --> @if($activeContext === 'products') <div class="modal"> <h4>{{ $activeId ? 'Edit Product' : 'Add Product' }}</h4> <!-- Relationship form is stored under the relationship name in the $form buffer --> <input type="text" wire:model.blur="form.products.name"> <input type="number" wire:model.blur="form.products.price"> <button wire:click="save">Save</button> <button wire:click="cancel">Cancel</button> </div> @endif </div>
4. Using Enums for Selects
The primary way to handle selection elements (selects, radios, etc.) is by implementing the AutoFormOptions interface on your Model or Enum.
The Enum:
enum Status: string implements AutoFormOptions { case DRAFT = 'draft'; case PUBLISHED = 'published'; public static function getOptions(?string $labelMask = null): array { return [ self::DRAFT->value => 'The Draft', self::PUBLISHED->value => 'Live on Site', ]; } }
The Blade View:
<select wire:model.blur="form.status"> <option value="">Select Status</option> @foreach($this->optionsFor('status') as [$value, $label]) <option value="{{ $value }}">{{ $label }}</option> @endforeach </select>
Automatic Fallback:
If you don't implement AutoFormOptions, the package will automatically generate readable labels from your Enum case names or Model attributes!
5. Listening for Events (Notifications)
You can listen for the events dispatched by the component to show "Saved" notifications or other UI feedback.
The Blade View (using Alpine.js):
<div x-data="{ show: false, message: '' }" x-on:saved.window="show = true; message = 'Changes saved!'; setTimeout(() => show = false, 2000)" x-on:field-updated.window="show = true; message = 'Field updated!'; setTimeout(() => show = false, 2000)"> <div x-show="show" class="notification" style="display: none;"> <span x-text="message"></span> </div> <!-- your form content ... --> </div>
6. List & Edit Pattern
You can use a single component to manage a collection of models, allowing you to select and edit any record from a list, or create a new one, all within the same view state.
The Livewire Component:
class ManageProducts extends AutoForm { public function mount() { $this->setModel(new Product); } public function rules(): array { return [ 'name' => 'required', 'price' => 'numeric', ]; } }
The Blade View:
<div> <!-- 1. The List --> <ul> @foreach(Product::all() as $product) <li> {{ $product->name }} <button wire:click="edit('', {{ $product->id }})">Edit</button> <button wire:click="delete('', {{ $product->id }})">Delete</button> </li> @endforeach </ul> <button wire:click="add('')">Create New Product</button> <hr> <!-- 2. The Edit/Create Form --> <h3>{{ $form->rootModelId ? 'Edit Product' : 'New Product' }}</h3> <input type="text" wire:model.blur="form.name"> <input type="number" wire:model.blur="form.price"> <button wire:click="save">Save Product</button> <button wire:click="cancel">Reset Form</button> </div>
7. Multi-Step Wizard
For complex forms, extend AutoWizardForm to break them into steps. It handles step navigation, explicit field mapping, and per-step validation.
The Livewire Component:
class UserWizard extends AutoWizardForm { public array $structure = [ 'profile' => ['name'], 'address' => ['city'], ]; public string $stepViewPrefix = 'livewire.steps.'; public function rules(): array { return [ 'name' => 'required', 'city' => 'required', ]; } public function mount(User $user) { $this->setModel($user); parent::mount(); } }
The Main Blade View:
<form wire:submit.prevent="submit"> @foreach($this->getSteps() as $index => $step) @include('livewire.steps.' . $step, ['isActive' => $this->isStepActive($index)]) @endforeach <button type="submit">{{ $this->isLastStep() ? 'Finish' : 'Next' }}</button> </form>
8. Redirecting after Save
If you need to perform actions after a successful save, such as redirecting the user to another page, you can easily override the save() method in your component.
The Livewire Component:
class CreatePost extends AutoForm { public function mount(Post $post) { $this->setModel($post); } public function rules(): array { return [ 'title' => 'required', 'content' => 'required', ]; } public function save(): void { // 1. Call the parent save logic to persist the data parent::save(); // 2. Perform your redirect $this->redirect(route('posts.index')); } }
Multi-Step Wizards
The AutoWizardForm extends the core AutoForm functionality to support complex, multi-step workflows with ease. It handles step navigation, per-step validation, and a unified submission flow.
Magic Features
- Unified
submit()Flow: A singlesubmit()method handles both navigating to the next step (with validation) and final persistence when the last step is reached. - Progress Integrity: Before final saving, the wizard performs an integrity check to ensure that all fields defined in your
rules()were actually present in at least one of the steps. If any field is missing, aLivewireAutoFormExceptionis thrown.
Configuration
To create a wizard, extend AutoWizardForm and configure the following:
$structure: A map of Blade view names to field names (e.g.,['step-one' => ['field1', 'field2'], 'step-two' => ['field3']]).$stepViewPrefix: A prefix for the views defined in$structure(e.g.,livewire.user-wizard-steps.).rules(): Define your validation rules.mount(): Initialize the model withsetModel($model).
API Reference
| Method | Description |
|---|---|
submit() |
Handles both transitions and final saving. Calls next() or save(). |
next() |
Validates current step's fields and moves forward. |
previous() |
Moves to the previous step. |
isLastStep() |
Returns true if on the final step. |
getSteps() |
Returns the list of defined step views. |
isStepActive(int $index) |
Checks if a step is currently active. |
Full Example
1. The Livewire Component:
namespace App\Livewire; use App\Models\User; use SchenkeIo\LivewireAutoForm\AutoWizardForm; class UserWizard extends AutoWizardForm { public array $structure = [ 'profile' => ['name', 'email'], 'address' => ['city'], 'preferences' => ['marketing_opt_in'] ]; public string $stepViewPrefix = 'livewire.user-wizard-steps.'; public function mount(User $user) { $this->setModel($user); parent::mount(); } public function rules(): array { return [ 'name' => 'required|min:3', 'email' => 'required|email', 'city' => 'required', 'marketing_opt_in' => 'boolean', ]; } }
2. The Main Blade View (user-wizard.blade.php):
<form wire:submit.prevent="submit"> @foreach($this->getSteps() as $index => $step) @include('livewire.user-wizard-steps.' . $step, [ 'isActive' => $this->isStepActive($index) ]) @endforeach <div class="actions"> @if($currentStepIndex > 0) <button type="button" wire:click="previous">Previous</button> @endif <button type="submit"> {{ $this->isLastStep() ? 'Finish' : 'Next' }} </button> </div> </form>
3. Individual Step View (profile.blade.php):
<div class="{{ $isActive ? 'block' : 'hidden' }}"> <input type="text" wire:model="form.name"> <input type="email" wire:model="form.email"> </div>
API Definitions
The package provides two main classes for managing form state and persistence: AutoForm and AutoWizardForm. These classes are Base Components, offering a context-aware "Single Buffer" architecture.
Using AutoForm
To use the package, extend AutoForm in your Livewire component and initialize it in the mount() method:
use SchenkeIo\LivewireAutoForm\AutoForm; class MyComponent extends AutoForm { public function mount(User $user) { $this->setModel($user); } public function rules(): array { return $this->scanInheritedRules($this->ruleKeys()); } public function ruleKeys(): array { return [ 'name', 'email', 'posts.title' // Relation support ]; } }
Using AutoWizardForm
For multi-step workflows, extend AutoWizardForm. It provides step management and per-step validation.
See the Multi-Step Wizards guide for details.
Public Properties (AutoWizardForm)
| Property | Type | Description |
|---|---|---|
$currentStepIndex |
int |
The zero-based index of the current active step. |
$structure |
array |
Map of Blade view names to field names. |
$stepViewPrefix |
string |
Prefix for the step Blade views. |
Public Methods (AutoWizardForm)
| Method | Description |
|---|---|
submit() |
Handles navigation (next step) or final submission (if on last step). |
next() |
Validates current step and moves forward. |
previous() |
Moves to the previous step. |
isLastStep() |
Returns true if on the final step. |
getSteps() |
Returns the list of defined step views. |
isStepActive(int $index) |
Checks if a step is currently active. |
Public Properties (AutoForm)
| Property | Type | Description |
|---|---|---|
$autoSave |
bool |
Default false. If true, fields are saved on every update (on blur). If false, you must call save() manually. |
$form |
FormCollection |
The internal state container (read-only from outside). |
The $form object (FormCollection)
The $form object contains the following state properties:
| Property | Type | Description |
|---|---|---|
activeContext |
string |
Current editing context: '' for root model or a relation name. |
activeId |
`int | string |
rootModelClass |
string |
The class name of the main model. |
rootModelId |
`int | string |
autoSave |
bool |
Whether auto-save is currently enabled. |
View Actions (Public Interface)
edit(string $relation, int|string $id)
Switches the context to edit a record.
- Related record:
wire:click="edit('posts', {{ $post->id }})" - Root model:
wire:click="edit('', {{ $otherId }})"
add(string $relation)
Switches the context to "Add Mode".
- Related record:
wire:click="add('posts')" - New root model:
wire:click="add('')"
save()
Validates and persists the current buffer data.
- If editing a relation, it returns to root context after saving.
- If creating a root model, it updates
rootModelIdafter the first save.
cancel()
Resets context to root ('') and reloads data to discard changes.
delete(string $relation, int|string $id)
Deletes or detaches a record and updates the active context.
Calling Methods from Blade
Since the form logic is now part of the Component itself, you can call methods directly in Blade without any special wrappers:
<button wire:click="save">Save</button>
This is fully compatible with Alpine.js and component libraries like Flux.
Helper Methods
getRelationList(string $relation)
Returns a collection of related models for the given relation name.
- Example:
@foreach($this->getRelationList('posts') as $post)
isEdited(string $relation, int|string $id)
Returns true if the specified related record is currently being edited.
- Example:
<li class="{{ $this->isEdited('posts', $post->id) ? 'active' : '' }}">
getModel()
Returns the root model instance with current buffer data applied.
getActiveModel()
Returns the model instance for the current active context (root or relation) with current buffer data applied.
ruleKeys()
Defines which model fields are included in the form and should have their validation rules inherited. Returns an array of strings (e.g., ['name', 'posts.title']).
scanInheritedRules(array $ruleKeys, array $rules = [])
Scans for rules from the active model and its relationships. This is used by the default rules() implementation to avoid duplicating validation rules already defined in your Eloquent models.
$ruleKeys: Array of field names (e.g.,['name', 'posts.title']) to scan for.$rules: Optional starting array of rules (component rules take precedence).
Returns the merged rules array.
Strict Validation: If a key in ruleKeys() cannot be resolved on the active model or if a relationship path is invalid, a LivewireAutoFormException is thrown.
optionsFor(string $key, ?string $labelMask = null)
Universal helper for fetching [value => label] pairs for selection elements (selects, radios, checkboxes).
Usage in Blade:
<select wire:model.blur="form.status"> @foreach($this->optionsFor('status') as [$value, $label]) <option value="{{ $value }}">{{ $label }}</option> @endforeach </select>
Primary Usage (AutoFormOptions Interface)
The recommended way to provide options is by implementing the AutoFormOptions interface on your Model or BackedEnum. This centralizes the logic and supports custom label formatting via the $labelMask.
class Country extends Model implements AutoFormOptions { public static function getOptions(?string $labelMask = null): array { return self::pluck('name', 'id')->toArray(); } }
Automatic Fallback
If the target class does not implement AutoFormOptions, the system uses an automatic generation strategy:
- Models (Relations):
- If
$labelMaskis null, it uses thenamecolumn. - If
$labelMaskis a string (e.g.,'title'), it uses that column as the label. - If
$labelMaskcontains placeholders in parentheses, it replaces them with the corresponding column values, e.g.,(first_name) (last_name).
- If
- Enums:
- If
$labelMaskis null, it generates a headline from the case name (e.g.,ACTIVE_STATUS->Active Status). - If
$labelMaskis provided, it must contain(name)or(value)keywords in parentheses.
- If
Labels generated via the fallback are automatically localized using Laravel's __().
Localization
The package supports both JSON and PHP-based localization storage. All labels generated for select options, whether via AutoFormOptions or the automatic fallback, are passed through Laravel's __() helper.
JSON-based Localization
Useful for full-sentence labels.
{
"Active": "User is Active",
"Pending": "User is Pending"
}
PHP-based Localization
Useful for structured keys.
// lang/en/enums.php return [ 'status' => [ 'active' => 'Active User', 'pending' => 'Review Pending' ] ];
Advanced Configuration (Replacements)
You can provide translation replacements by returning an array from getOptions():
public static function getOptions(?string $labelMask = null): array { return [ 'active' => [ 'key' => 'enums.status.active_count', 'replace' => ['count' => User::where('active', true)->count()] ] ]; }
The AutoFormLocalisedEnumOptions Trait
For Enums, you can use the AutoFormLocalisedEnumOptions trait to automate key generation:
enum UserStatus: string implements AutoFormOptions { use AutoFormLocalisedEnumOptions; const OPTION_TRANSLATION_PREFIX = 'enums.user_status'; case ACTIVE = 'active'; }
This automatically resolves to the translation key enums.user_status.active. You can also override the prefix dynamically by passing it as the $labelMask to optionsFor('status', 'custom.prefix').
Exceptions
The package throws SchenkeIo\LivewireAutoForm\Helpers\LivewireAutoFormException for:
- Configuration Integrity: Errors in setup.
- Rules Discrepancy: Mismatches between data and
rules(). - Relation Errors: Unsupported relationship types.
- Enum Errors: Missing enum casts.
Persistence & Relationship Discovery
Livewire Auto Form features a hardened persistence layer that ensures data is correctly routed between your form buffer and the database, with a specific focus on Eloquent relationships.
Strict Relationship Validation
The package distinguishes between actual Eloquent relationships and other dot-notated data (such as JSON casts or nested arrays) by using the model's isRelation() method.
When you define dot-notated rules like:
public function rules(): array { return [ 'author.name' => 'required', // Eloquent relation 'settings.theme' => 'string', // JSON cast field ]; }
The package performs the following validations:
- Rule Discovery: During initialization and rule inheritance, only keys that correspond to verified Eloquent relations are treated as relationship contexts.
- Persistence Protection: When saving data, the package verifies that a path segment is an actual relationship before attempting to invoke it as a method on the model.
If a dot-notated key does not correspond to a relationship, it is treated as a standard model attribute, allowing it to work seamlessly with Laravel's built-in support for JSON casts.
Relationship Hardening
The CrudProcessor has been hardened to prevent common errors when dealing with complex relationship trees:
- Path Resolution: When resolving nested relationships (e.g.,
brand.category.name), every segment of the path is validated against the model's relationship definitions. - Error Handling: If a path segment is not a valid relation, a
LivewireAutoFormExceptionis thrown, providing clear feedback on the configuration error instead of failing with aBadMethodCallException. - Safe Method Invocation: Before calling a relationship method during the save process, the package explicitly checks if it exists and is a valid relation, preventing accidental execution of other model methods.
Event System
Livewire Auto Form dispatches several browser events that you can listen for in Alpine.js or Livewire to provide real-time feedback to your users.
Available Events
1. saved
Dispatched after the entire form (or the active relationship context) has been successfully persisted to the database.
Parameters:
context(string): The relationship context that was saved (empty string for the root model).id(int|string|null): The ID of the model that was saved.
Example (Alpine.js):
<div x-on:saved.window="alert('Saved model ' + $event.detail.id + ' in context ' + $event.detail.context)"> <!-- ... --> </div>
2. field-updated
Dispatched when an individual field is automatically saved to the database (only when autoSave is enabled).
Parameters:
changed(string): The name of the field that was updated.context(string): The relationship context where the change occurred.id(int|string|null): The ID of the model that was updated.
Example (Alpine.js):
<div x-on:field-updated.window="console.log('Field ' + $event.detail.changed + ' was updated!')"> <!-- ... --> </div>
3. confirm-discard-changes
Dispatched when a user action might result in losing unsaved changes in the buffer. This only happens when autoSave is disabled and the form buffer is not empty.
Parameters: None.
Listening in Livewire
You can also listen for these events in other Livewire components using the #[On] attribute:
use Livewire\Attributes\On; #[On('saved')] public function handleSave($context, $id) { // Do something when a model is saved }
Markdown file generated by schenke-io/packaging-tools
