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
- hidden:false
- label:"Account type"
- name:"type"
- options:[{"label":"Business","value":"business"},{"label":"Individual","value":"individual"}]
- prefillRefreshOn:null
- prefillResetOn:null
- readOnly:false
- required:false
- tabIndex:null
- tooltip:null
- value:null
- autoComplete:null
- autoFocus:false
- columnWidth:"md"
- conditions:{"visible":[{"field":"type","operator":"eq","value":"business"}]}
- dependsOnAny:false
- dependsOnKeys:null
- disabled:false
- editablePrefill:false
- helperText:null
- hidden:false
- label:"VAT ID"
- name:"vat"
- placeholder:null
- prefillRefreshOn:null
- prefillResetOn:null
- prefix:null
- readOnly:false
- required:false
- suffix: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
- hidden:false
- label:"Country"
- name:"country"
- options:[{"label":"Germany","value":"DE"},{"label":"Austria","value":"AT"},{"label":"United States","value":"US"}]
- prefillRefreshOn:null
- prefillResetOn:null
- readOnly:false
- required:false
- tabIndex:null
- tooltip:null
- value:null
- autoComplete:null
- autoFocus:false
- columnWidth:"md"
- conditions:{"required":[{"field":"country","operator":"eq","value":"DE"}]}
- dependsOnAny:false
- dependsOnKeys:null
- disabled:false
- editablePrefill:false
- helperText:null
- hidden:false
- label:"VAT ID"
- name:"vat"
- placeholder:null
- prefillRefreshOn:null
- prefillResetOn:null
- prefix:null
- readOnly:false
- required:false
- suffix: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']);
NumberInput::make('guardian', 'Guardian name') ->requiredWhen('age', '<', 18);The operator accepts a comparison string or an Op case (Lattice\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 | empty, filled | Op::Empty, Op::Filled |
| dates | before, after | Op::Before, Op::After |
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.
A shared truth-table fixture (tests/Fixtures/condition-operator-parity.json) is run by both a PHP
test and a TypeScript test, so the server (ConditionEvaluator) and client (evaluateOp) evaluators
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'));When the computation also needs to change other parts of the field, use ->dependsOn() to name the
fields to watch and receive the field itself in the closure.
TextInput::make('total', 'Total') ->dependsOn( ['qty', 'price'], fn (TextInput $field, FormData $data) => $field->value($data->float('qty') * $data->float('price')), );A computed value is authoritative by default: the closure result overwrites whatever the user typed on every resolve and on submit, so the field reads as derived.
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']);