K

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.

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 native wire:confirm dialog 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.