Forms
A form is a PHP class that declares its fields and handles its own submission. You define the schema once; Lattice renders the React inputs, posts to a dedicated endpoint, validates with Laravel, and runs your handler. Validation runs live as the user types, using the exact same rules.
Defining a form
Section titled “Defining a form”Extend FormDefinition and implement two methods: definition() builds the field schema, and
handle() processes a valid submission. The #[AsForm] attribute gives the form a stable id so it can
be discovered and addressed by its own endpoint.
use Illuminate\Http\Request;use Lattice\Form\Attributes\AsForm;use Lattice\Form\Components\Form as FormComponent;use Lattice\Form\Components\TextInput;use Lattice\Form\FormData;use Lattice\Form\FormDefinition;use Symfony\Component\HttpFoundation\Response;
#[AsForm('app.profile.form')]class ProfileForm extends FormDefinition{ public function definition(FormComponent $form, Request $request): FormComponent { return $form->schema([ TextInput::make('name', 'Name')->rules(['required', 'string', 'max:255']), TextInput::make('email', 'Email')->email()->rules(['required']), ]); }
public function handle(FormData $data, Request $request): Response { $request->user()->update($data->only('name', 'email'));
return redirect('/profile'); }}The schema accepts fields and layout containers (Card, Grid, Stack) in any nesting — see
Fields for the field types and Components for layout.
Rendering a form
Section titled “Rendering a form”Render a form on a page with Form::use(), passing the definition class. Configure it fluently:
use Lattice\Ui\Enums\HttpMethod;use Lattice\Form\Components\Form;
Form::use(ProfileForm::class) ->method(HttpMethod::Patch) ->submitLabel('Save changes') ->fill([ 'name' => $user->name, 'email' => $user->email, ]);->fill()seeds the fields for an edit form. A field’s filled value wins over its->value()default, and fields can react to it (aSelectresolves stored ids to labels, for example).->method()sets the HTTP verb the form submits with (postby default).->submitLabel()sets the submit button’s text.->context()passes extra data (such as the record id) thathandle()can read back.
The submit lifecycle
Section titled “The submit lifecycle”Every form posts to its own endpoint, resolved from its #[AsForm] id. The request is signed, so a
form only accepts submissions for the schema it actually rendered. FormController routes the request:
- A search request (a searchable
Selectfetching options) returns matching options. - A resolve request (a dependent field recomputing) returns the updated field nodes and values.
- A precognitive request validates and returns
204/422without runninghandle(). - Otherwise the submission is validated once, then passed straight to
handle().
The endpoint validates internally and hands handle() the result — you never call validate()
yourself inside handle(). The data it receives is already validated and cast: visible-only,
with hidden values and locked (disabled or read-only) user input stripped, and a server-set
->value() on a locked field still comes through. Your handle() returns any Laravel Response or
Responsable: a redirect, a JSON payload, or a toast effect.
The handle() signature
Section titled “The handle() signature”handle() accepts any combination of parameters, resolved in this order: by name ($data for the
validated FormData, $request for the Request), then by type (FormData, Request), then from
the Laravel container. Declare only what you need:
public function handle(FormData $data): Response { /* … */ }public function handle(FormData $data, Request $request): Response { /* … */ }public function handle(Request $request): Response { /* … */ } // no validated data neededOlder handle(Request $request) signatures keep working, reading raw input off $request directly —
but don’t call validate() again inside handle(): the endpoint already validated the submission
once before calling you, and calling it a second time just re-runs the same rules. Add FormData $data to the signature instead.
Resetting after submit
Section titled “Resetting after submit”Control what the form does with its fields once a submission resolves:
$form ->resetOnSuccess() // clear every field after a successful submit ->resetOnError(['password']); // clear only these fields after a failed submitBoth accept true/false to reset all fields or none, or an array of field names to reset a subset.
resetOnError(['password']) is the common case — clear the password but keep what the user typed
everywhere else.
Working with submitted data
Section titled “Working with submitted data”FormData (Lattice\Form\FormData) extends Laravel’s ValidatedInput, so the full
InteractsWithData API is available — typed accessors, only()/except(), collect(),
array access, and dot-notation get():
$data->string('name'); // Stringable — call ->toString() to get a plain string$data->boolean('subscribe');$data->integer('quantity');$data->float('price');$data->get('tags', []);$data->only('name', 'email');$data['name'];Returning a Stringable or an enum from a field’s value() closure is fine — field values
normalize them to their scalar on the way to the wire, so no ->toString() is needed there.
The same object shows up wherever a callback needs to read the in-flight form state —
dynamic rules and dependent fields —
as well as in handle().
Live validation with Precognition
Section titled “Live validation with Precognition”Call ->precognitive() in the definition to validate as the user types, through
Laravel Precognition. The form sends a debounced request
that runs your rules and returns messages without executing handle(). Because it is the same
server-side ruleset, there is nothing to keep in sync.
public function definition(FormComponent $form, Request $request): FormComponent{ return $form ->precognitive(500) // debounce in milliseconds ->schema([/* … */]);}See Validation for the full rule surface.
The submit button
Section titled “The submit button”The form renders its submit button for you, labelled by ->submitLabel(). It is form-aware: it
disables while submitting and while there are validation errors, and shows a spinner — the heading of
that error summary is set with ->validationSummaryLabel() (default “Fix these fields to continue:”).
The submit row itself is a plain flex row (right-aligned by default) — as of this version it no longer
renders inside a bordered surface bar. Use ->submitJustify(), ->submitVariant(), and ->submitEmphasis()
to adjust its alignment and button style:
use Lattice\Form\Components\Form;use Lattice\Ui\Enums\Emphasis;use Lattice\Ui\Enums\Justify;
Form::use(ProfileForm::class) ->submitLabel('Save changes') ->submitJustify(Justify::Start) ->submitEmphasis(Emphasis::Outline);To replace the row’s buttons entirely, call ->submitButtons() with one or more Button components.
A button marked ->submit() keeps the managed submit button’s spinner, disabled-while-invalid state,
and error-summary tooltip — the others render as plain buttons:
use Lattice\Ui\Components\Button;
$form->submitButtons( Button::make('Cancel'), Button::make('Save draft')->submit(),);Only the label and variant of a ->submit()-marked button carry over to the managed submit button —
other props such as icon or size do not.
To take over placement entirely — render the button somewhere other than the form footer — call
->withoutSubmitButton(), which still removes the whole row, and place a
Button with ->submit() in the schema yourself:
use Lattice\Ui\Components\Button;use Lattice\Ui\Enums\Emphasis;
Button::make('Create account')->submit();Next steps
Section titled “Next steps”- Fields — every field type and the options they share.
- Validation — rules, messages, and live validation.
- Conditional fields — show, require, or compute fields from other fields.