Forms
Reactive Fields
Fields that react to each other without leaving the schema — which closures see live state, when they re-run, and what each round trip costs.
On this page
Fields can react to each other's values without leaving the schema. Every dynamic field
closure — visible(), hidden(), disabled(), afterStateUpdated() — is evaluated with
Filament-style state accessors resolved against the live Livewire state bag. The same API
works in a standalone WithForms form and inside a table action modal.
State accessors
| Accessor | Returns |
|---|---|
$get('field') |
Another field's current value (a sibling under the same statePath). |
$get() |
This field's own value, read live — reflects a $set made earlier in the same closure. |
$set('field', $value) |
Writes another field. StateContainer-safe, so it also works inside action modals. |
$state |
This field's value, snapshotted when the closure is invoked. |
$component |
The field instance. |
Reach for $get / $set instead of Livewire::current() + data_get() — the accessors resolve
the correct bag prefix for you, including the tableState.modal.action.formData bag of an action
modal.
Conditional fields
Show a field only when another field has a given value. A field hidden this way is also skipped
during validation, so a required() rule never blocks submit while the field is hidden:
Select::make('type') ->options(['business' => 'Business', 'person' => 'Person']) ->live(), TextInput::make('vat_id') ->visible(fn ($get) => $get('type') === 'business') ->required(),
The trigger field must be ->live() so its change round-trips to the server and re-evaluates the
dependent field's visibility.
Layout components receive the same accessors, so a whole Grid, Section or Fieldset can show or
hide on a sibling's value:
Section::make('Billing') ->schema([ TextInput::make('vat_id'), TextInput::make('company'), ]) ->visible(fn ($get) => $get('type') === 'business'),
Field actions and buttons
Attach an interactive Action to an input's affix or hint area with suffixAction(),
prefixAction() or hintAction(). The action's callback runs on the server with the same
$get / $set / $state context — ideal for a lookup, a "generate" helper, or an inline verify
button:
use NyonCode\WireCore\Actions\Action; TextInput::make('title')->suffixAction( Action::make('to_upper') ->icon('heroicon-o-arrow-up') ->action(fn ($get, $set) => $set('title', strtoupper((string) $get('title')))),);
For a standalone, design-system-styled button bound to a closure — instead of a raw
Html::make()->content('<button …>') that bypasses the palette — use the Button field. Its
presentation (label / icon / color / size / outlined) mirrors actions:
use NyonCode\WireForms\Components\Button; Button::make('generate_slug') ->label('Generate slug') ->icon('heroicon-o-sparkles') ->action(fn ($get, $set) => $set('slug', Str::slug((string) $get('title')))),
Both work in a standalone WithForms form and inside a table action modal — the host's
callFieldAction() endpoint re-resolves the field from the live schema and runs the closure.
afterStateUpdated()
Run a callback after a field's value changes. Registering a callback automatically enables
live() — without a server round-trip the hook could never fire. The callback receives the new
value as $state, the previous value as $old, plus $get / $set / $component:
TextInput::make('type') ->afterStateUpdated(function ($state, $old, $get, $set) { // Auto-fill a dependent field from the value just entered. $set('vat_id', $state === 'business' ? null : ''); });
Any subset of the parameters can be type-hinted in any order — they are resolved by name:
TextInput::make('quantity') ->afterStateUpdated(fn ($state, $set) => $set('total', $state * 10));
All of this reactivity — afterStateUpdated(), live validation, field actions, remote
select search and conditional visibility (visibleWhen() / visible(fn ($get) => …)) — also
works for fields inside Repeater items: the dispatch resolves the field per item, and
$get/$set read and write that item's own bag (so $set('slug', …) on row 2 touches only
row 2). A conditional field inside a repeater shows or hides based on its own item's state,
not its siblings'.
Multi-step forms get the same treatment: inside a Livewire host the standalone Wizard validates the current step on the server before "Next" advances, and a failed submit jumps to the first step containing an error.
Prefill a form from an action
An action modal form reads and writes the modal.action.formData bag. Seed its initial values with
fillFormUsing(). The callback receives the record for row actions (and null for header actions,
which have no record):
use NyonCode\WireCore\Actions\EditAction; EditAction::make() ->form([ TextInput::make('name')->required(), Select::make('role')->options(Role::class), ]) ->fillFormUsing(fn ($record) => [ 'name' => $record->name, 'role' => $record->role->value, ]);
Inside that form the reactive accessors above behave exactly as in a standalone form — $get,
$set and afterStateUpdated() all resolve against the live modal bag.
Modal footer actions
Add extra buttons to an action modal's footer with modalFooterActions(). Each
ModalFooterAction callback receives the live form-data bag as $data, a $set writer for it,
$component, and the modal's $record / $records context — so a footer button can read and
write the in-progress form without submitting it:
use NyonCode\WireCore\Actions\ModalFooterAction; EditAction::make() ->form([ TextInput::make('name')->required(), TextInput::make('slug'), ]) ->modalFooterActions([ ModalFooterAction::make('generate_slug') ->label('Generate slug') ->icon('sparkles') ->action(fn ($data, $set) => $set('slug', Str::slug($data['name'] ?? ''))), ModalFooterAction::make('preview') ->position('after') // 'before' (default) or 'after' the Cancel/Submit buttons ->submitsForm() // validate the form before the callback runs ->closesModal() // close the modal afterwards ->action(fn ($data, $component) => $component->dispatch('preview', data: $data)), ]);
->submitsForm()validates the modal form first, so validation errors surface before the callback.->closesModal()closes the modal once the callback returns.->position('before'|'after')places the button before or after the built-in Cancel/Submit.->requiresConfirmation()asks the user before the callback runs (a nativewire:confirmdialog with a translated default message);->confirm('Really reset?')sets a custom message.
Field partials
A live() field commits to the server on every change, and the server answers by
re-rendering the host component. On a 12-field form that is 19 860 B of HTML
to carry one field's 1 562 B — 12.7× raw, 2.3× gzipped — plus the browser
morphing all of it.
fieldPartials() makes the form answer with the fields that moved instead:
public function form(Form $form): Form{ return $form ->statePath('data') ->fieldPartials() ->schema([ TextInput::make('name')->live(), TextInput::make('summary') ->helperText(fn () => 'Summary for '.$this->data['name']), ]);}
What a commit answers with
Three outcomes, and the most common one sends nothing at all:
| what changed | what comes back |
|---|---|
| nothing — an ordinary keystroke | neither a view nor a region |
| one or more fields' markup | those fields, as regions |
the set of fields — a visibleWhen() sibling appeared or disappeared |
a full render |
The first row surprises people, and it is the point: a field's value rides
wire:model and the data payload, not its markup. A TextInput renders no
value attribute at all, so committing one changes no markup anywhere and there
is genuinely nothing to send. Markup moves only when something derived moves — a
sibling whose options(), label() or helperText() closure reads the state, a
validation error appearing, a field becoming disabled.
How it decides
By rendering the fields and comparing each one's markup against what it last
sent — not by working out which fields depend on which. That is deliberate: a
dependency graph would have to understand every closure a field's configuration
can hold, and the one it missed would silently show a stale value. Different
markup is the only thing this has to notice, and it notices it the same way
Table::rowPartials() notices a changed row.
What you trade
The host's own view does not re-render on a covered commit. Anything it draws outside the form — a live preview of the data, a heading counting filled fields — keeps its previous value until the next full render:
<div> <h2>{{ $this->data['name'] }}</h2> {{-- will not update on a field commit --}} {{ $this->form }}</div>
If a form's page depends on that, leave the flag off. Nothing else about the form
changes: visibleWhen() siblings stay correct on their own, because a field
appearing or disappearing is a shape change that falls back to a full render.