From 2eb4f019ff56d9138d6c49ad2e4c9fee24be4026 Mon Sep 17 00:00:00 2001 From: Kamil Sztandur Date: Sun, 20 Sep 2026 21:46:19 +0200 Subject: [PATCH] Skill and docs: making a prefilled field react to a dependency subscribeToFields never reaches a field the user has not edited; that is the model and stays. Document the way to do it when a prefilled field must react anyway: validate() from a listener on the watched field, with its two traps. Co-Authored-By: Claude Fable 5.1 --- docs/faq.mdx | 4 ++++ docs/validation/cross-field.mdx | 5 ++++- skills/advanced_forms/SKILL.md | 23 +++++++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/docs/faq.mdx b/docs/faq.mdx index 8de0068..5811d6b 100644 --- a/docs/faq.mdx +++ b/docs/faq.mdx @@ -35,6 +35,10 @@ Two gates sit in front of it: the dependent field must be in a mode other than ` it at least once. Under `manual` the mismatch shows up on submit instead. Also check that the *value* of a watched field changed — a status-only change never fires it. See [Cross-field logic](./validation/cross-field.mdx). +If the dependent field was prefilled and really has to react before anyone touches it, call `validate()` on it from a +listener on the watched field: `validate()` ignores both gates. Keep the rule in the validator; the listener only +decides when it runs. + diff --git a/docs/validation/cross-field.mdx b/docs/validation/cross-field.mdx index a034009..32bbe68 100644 --- a/docs/validation/cross-field.mdx +++ b/docs/validation/cross-field.mdx @@ -82,7 +82,10 @@ class _PasswordFormState extends State { - It re-runs **this** field's **sync** validator. It does not copy or derive values, and it does not re-run the async validator — this field's own value did not change, so its verdict still stands. - It obeys the [gate](./modes.mdx): nothing happens while this field is in `ValidationMode.manual`, and nothing on a - field the user has never edited. Under `manual` the mismatch shows up on submit instead. + field the user has never edited. Under `manual` the mismatch shows up on submit instead. When a prefilled field has to react + before anyone touches it — an instructor loaded from the server, an aircraft they may not fly — call `validate()` on + it from a listener on the watched field; `validate()` ignores the gate. Compare the watched value yourself, since a + listener also fires on status changes, and remember that `validate()` runs the async validator too. - It fires when a watched **value** changes, never on a status-only change of the sibling, so it cannot loop. - A second call replaces the previous subscription; the subscription is dropped on `dispose`. - A code pushed with `setError` gives way to whatever the validator now returns, like any other re-run. diff --git a/skills/advanced_forms/SKILL.md b/skills/advanced_forms/SKILL.md index d8befb5..a02b0e7 100644 --- a/skills/advanced_forms/SKILL.md +++ b/skills/advanced_forms/SKILL.md @@ -475,6 +475,29 @@ children.subscribeToFields([adults]); only `adults` lights up live — `children` shows its half at the first `validate()`. Both showing simultaneously before submit is not something the modes can give you; render the pair's message once, from the form, if the design needs it. +- **When a prefilled field MUST react anyway** — the instructor came from the server, the user + switches the aircraft, and the instructor must show "not rated" although nobody touched that + field — `subscribeToFields` cannot do it (rule 2). Do not reach for `setError` to hand-write + the error next to the validator. Call `validate()` on the dependent field from a listener on + the watched field: `validate()` ignores the gate and the mode, so it reaches an untouched + field, and the sync result lands synchronously. Compare the value yourself, because a listener + also fires on status changes: + + ```dart + // In the form constructor, after registerFields. The rule itself stays in the + // instructor's validator; this only decides WHEN it runs. + var lastAircraft = aircraft.fieldValue; + aircraft.addListener(() { + if (aircraft.fieldValue == lastAircraft) return; + lastAircraft = aircraft.fieldValue; + instructor.validate(); // fire-and-forget; also runs the async validator, if any + }); + ``` + + Two traps: a `validate()` already in flight is shared, so a second call before it finishes + gets the first round's result — one value change per event-loop turn is fine, a synchronous + loop of writes is not; and `validate()` also runs `asyncValidation`, so a field with a server + check will hit the server on every change of the watched field. **Value depends on another field** ("when B changes, set A" — totals, mirroring, clearing a dependent selection). Use the form's `addRelation(source, select, onChange)`, in the