schenke-io / livewire-auto-form
Enhanced livewire component to edit models and its relationships
Installs: 14
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-relation-manager: ^2.0
- schenke-io/packaging-tools: ^0.2
- spatie/laravel-ray: ^1.40
This package is auto-updated.
Last update: 2026-02-01 14:25:09 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, any attribute that implements
Livewire\Wireable(like Enums) is automatically flattened to its scalar value if 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.
Installation
composer require schenke-io/livewire-auto-form
- Livewire Auto Form
- Concept of Coding
- Code Examples
- Multi-Step Wizards
- API Definitions
- 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)
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 [ 'name' => 'required', 'email' => 'required|email', 'posts.title' => 'required' // 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.
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.
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
