Conditional fields
Fields can react to the rest of the form. A condition compares another field’s value and toggles this field’s visibility, required, read-only, or disabled state. Conditions are evaluated on the client as the user types, and the same rules are re-checked on the server, so they cannot be bypassed.
Each method names the field to watch, an optional operator, and the value to compare against. Open the
Tree tab on the examples below to see the conditions that are serialized to the client.
Visible when
Section titled “Visible when”->visibleWhen() hides the field until the condition matches. Switch the account type below to
Individual and the VAT ID field disappears.
Choice::make('type', 'Account type')->options([ Choice::option('Business', 'business'), Choice::option('Individual', 'individual'),]);
TextInput::make('vat', 'VAT ID')->visibleWhen('type', 'business')- autoFocus:false
- columnWidth:"md"
- conditions:null
- dependsOnAny:false
- dependsOnKeys:null
- disabled:false
- editablePrefill:false
- helperText:null
- label:"Account type"
- labelAction:null
- name:"type"
- optionSchema:null
- options:[{"data":null,"description":null,"group":null,"label":"Business","tooltip":null,"value":"business"},{"data":null,"description":null,"group":null,"label":"Individual","tooltip":null,"value":"individual"}]
- prefillRefreshOn:null
- prefillResetOn:null
- readOnly:false
- required:false
- tabIndex:null
- tooltip:null
- value:null
- autoComplete:null
- autoFocus:false
- columnWidth:"md"
- conditions:{"disabled":[],"readOnly":[],"required":[],"visible":[{"field":"type","operator":"eq","value":"business"}]}
- copyable:false
- dependsOnAny:false
- dependsOnKeys:null
- disabled:false
- editablePrefill:false
- helperText:null
- label:"VAT ID"
- labelAction:null
- name:"vat"
- placeholder:null
- prefillRefreshOn:null
- prefillResetOn:null
- prefix:null
- prefixFieldName:null
- readOnly:false
- required:false
- suffix:null
- suffixFieldName:null
- tabIndex:null
- tooltip:null
- type:null
- value:null
Required when
Section titled “Required when”->requiredWhen() makes the field required only while the condition matches. With Germany
selected the VAT ID is required; pick another country and it becomes optional.
Choice::make('country', 'Country')->options([ Choice::option('Germany', 'DE'), Choice::option('Austria', 'AT'), Choice::option('United States', 'US'),]);
TextInput::make('vat', 'VAT ID')->requiredWhen('country', 'DE')- autoFocus:false
- columnWidth:"md"
- conditions:null
- dependsOnAny:false
- dependsOnKeys:null
- disabled:false
- editablePrefill:false
- helperText:null
- label:"Country"
- labelAction:null
- name:"country"
- optionSchema:null
- options:[{"data":null,"description":null,"group":null,"label":"Germany","tooltip":null,"value":"DE"},{"data":null,"description":null,"group":null,"label":"Austria","tooltip":null,"value":"AT"},{"data":null,"description":null,"group":null,"label":"United States","tooltip":null,"value":"US"}]
- prefillRefreshOn:null
- prefillResetOn:null
- readOnly:false
- required:false
- tabIndex:null
- tooltip:null
- value:null
- autoComplete:null
- autoFocus:false
- columnWidth:"md"
- conditions:{"disabled":[],"readOnly":[],"required":[{"field":"country","operator":"eq","value":"DE"}],"visible":[]}
- copyable:false
- dependsOnAny:false
- dependsOnKeys:null
- disabled:false
- editablePrefill:false
- helperText:null
- label:"VAT ID"
- labelAction:null
- name:"vat"
- placeholder:null
- prefillRefreshOn:null
- prefillResetOn:null
- prefix:null
- prefixFieldName:null
- readOnly:false
- required:false
- suffix:null
- suffixFieldName:null
- tabIndex:null
- tooltip:null
- type:null
- value:null
Read-only and disabled when
Section titled “Read-only and disabled when”->readOnlyWhen() and ->disabledWhen() toggle the read-only and disabled states the same way.
TextInput::make('coupon', 'Coupon') ->readOnlyWhen('plan', 'enterprise');
TextInput::make('coupon', 'Coupon') ->disabledWhen('billing', 'invoice');Operators
Section titled “Operators”With two arguments, the condition checks equality. Pass an array to match any of several values, or pass an operator as the middle argument for other comparisons.
TextInput::make('vat', 'VAT ID') ->visibleWhen('country', ['DE', 'AT', 'CH']);
TextInput::make('guardian', 'Guardian name') ->requiredWhen('age', '<', 18);The operator accepts a comparison string or an Op case (Lattice\Core\Enums\Op):
| Condition | String | Op case |
|---|---|---|
| equal | =, == |
Op::Equals |
| not equal | !=, <> |
Op::NotEquals |
| greater | >, >= |
Op::GreaterThan, Op::GreaterThanOrEqual |
| less | <, <= |
Op::LessThan, Op::LessThanOrEqual |
| substring | contains, starts_with, ends_with |
Op::Contains, Op::StartsWith, Op::EndsWith |
| membership | in, not_in |
Op::In, Op::NotIn |
| presence | — (use the Op case) |
Op::Empty, Op::Filled |
| dates | before, after |
Op::Before, Op::After |
For presence checks, pass the Op case — ->visibleWhen('coupon', Op::Filled). A bare 'empty' or
'filled' string with no third argument is treated as a value to compare against, not an operator.
Server and client agreement
Section titled “Server and client agreement”Conditions are evaluated twice: on the client as the user types, and again on the server so they cannot be bypassed. For that to be safe, both sides must coerce values identically. The contract is:
- Equality against a boolean coerces the value the way PHP’s
filter_var(…, FILTER_VALIDATE_BOOLEAN)does —1,true,on, andyesare true, anything else is false. Against a non-boolean, values compare as strings. - Numeric comparisons (
>,>=,<,<=) coerce both sides to numbers, so use them on numeric fields. contains/starts_with/ends_withcompare as strings.empty/filledtreatnulland the empty string as empty.before/afterparse both sides as dates; a value that cannot be parsed never matches.
Both evaluators are tested against one shared truth table, so the server and the client cannot drift apart.
Computed values
Section titled “Computed values”A field’s value can be derived from the form data instead of typed. Pass a closure to ->value() to
compute it from the current FormData. The closure runs on the server whenever the inputs change.
Closure parameters use Lattice’s closure evaluation utilities.
TextInput::make('total', 'Total') ->value(fn (FormData $data) => $data->float('qty') * $data->float('price'));A computed value is locked by default: the closure result overwrites whatever the user typed on every resolve and on submit, so the field reads as derived.
->dependsOn() reacts to other fields changing, receiving the field itself in the closure. It can
change anything about the field — its options, props, visibility, or live value via $field->value(...)
— but it never locks the field the way ->value(Closure) does: a value set from inside a dependsOn
callback ships to the client on the next resolve so the UI stays in sync, but a user who then types
into the field wins on submit.
TextInput::make('total', 'Total') ->dependsOn( ['qty', 'price'], fn (TextInput $field, FormData $data) => $field->value($data->float('qty') * $data->float('price')), );To make a computed value that reacts to specific fields and stays authoritative, use ->value()
alone — it already re-evaluates on every resolve, so naming dependencies is only needed for
dependsOn’s other effects (changing options, visibility, or other props).
Editable defaults
Section titled “Editable defaults”Pass editable: true to compute a suggestion instead — the value is applied as a default the user
can then override by typing. Once a field is manually edited it stops tracking the computed value.
TextInput::make('price', 'Price') ->value(fn (FormData $data) => priceFor($data->get('product')), editable: true);Two lists control when the suggestion re-applies:
resetOn— naming a field here clears a manual override when that field changes, so the suggestion takes over again.refreshOn— naming a field here recomputes the suggestion when that field changes, but only while the user has not overridden it.
TextInput::make('price', 'Price') ->value(fn (FormData $data) => priceFor($data->get('product')), editable: true, resetOn: ['product']);Deciding nothing
Section titled “Deciding nothing”Returning null is a decision: it clears the field. When a resolver has nothing to suggest —
no product picked yet, no price on file — return Resolution::Keep instead and the field keeps
whatever is in it, including what the user typed.
use Lattice\Form\Resolution;
TextInput::make('price', 'Price') ->value( fn (FormData $data) => priceFor($data->get('product')) ?? Resolution::Keep, editable: true, resetOn: ['product'], );Resolution::Keep works the same way in an authoritative ->value() resolver: the field keeps its
submitted value and stays non-authoritative for that pass, so validation does not overwrite it.