feat: rework validation modes - #54
Conversation
037f412 to
6bcb4e7
Compare
0226b80 to
797f304
Compare
797f304 to
21071e2
Compare
One named mode on the form says what the form does: `disabled` (the default), `onUserInteraction`, or `onUnfocus`. It is broadcast to every field and subform, including ones registered or attached later, so a section added after the first submit behaves like one added at build time. The decision lives in one new file, `lib/src/validation_mode.dart`: the enum, the internal event enum, and `validatesOn` — a pure function of the event, the mode and the interaction flag. It reads no field status, which is what makes it safe on a pipeline that overwrites the status with `pending` and clears both error slots on every write. Above every mode: a field the user has never edited validates nothing on its own, and a subtree with `validationEnabled` off validates nothing at all. The switch is folded into the mode the form broadcasts, so a field has one input to obey rather than two, and a switched-off subtree is excluded from `validate()` and from every derived aggregate alike. `validate()` neither consults the mode nor changes it — escalation is deleted, which is what makes a single broadcast sufficient. Also: - `prefill(value)` writes a value the user did not type, without arming the guarantee. - `focusNode` and `focus()` move up to `AdvancedFieldController`, and `handleUnfocus()` runs the blur path — flushing a debounce in any mode, reusing a settled verdict, retrying a failed round, and reporting a throwing validator instead of leaking it into the zone. - A field or subform may claim its own mode and manage it from then on; a parent's later change reaches only the children that have not. - `validateWithAutovalidate()` is renamed `revalidateSync()`. - `SharedCall.invalidate()` drops a pass answered under settings that no longer hold. - The error report tag says `leancode_forms`, not the stale `advanced_forms`. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cut the comment volume the validation-modes feature added, without changing behaviour: facts that were repeated across three to five doc-comments now live in one place and are linked from the rest. Rename three private members to stop them colliding with their neighbours: `_applyMode` -> `_publishValidationMode` (mirrors the form's peer), `_formEnabled` -> `_parentEnabled` (same concept as the form's field of that name), `_statusKeepingFailure` -> `_statusAfterAbort`. Add `AdvancedFieldController._validatesOn`, replacing three five-line `validatesOn(...)` calls, and inline the single-call-site `_flushDebounce` into `handleUnfocus`. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
6bcb4e7 to
d0d9e7f
Compare
Both controllers interleaved several concerns in one file. Each concern now lives in its own part file of the same library, so private state stays reachable and no cross-file contract is needed. Field controller: focus handling and the async validation round machinery. Form controller: child wiring, relations and the state class. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
| part of 'advanced_form_controller.dart'; | ||
|
|
||
| // Listeners on child fields and subforms, and what to update when they change. | ||
| mixin _ChildWiring on ChangeNotifier { |
There was a problem hiding this comment.
Is it a good idea of extracting it here? I'm worried that the public API will be harder to lookup - wdyt?
There was a problem hiding this comment.
As I said in the other comment, as long as it's mixin, not extension it's findable, then it's fine
There was a problem hiding this comment.
Splitting this logic into few mixins was a very good idea. Much more readable when logic is grouped
| exception: error, | ||
| stack: stack, | ||
| library: 'advanced_forms', | ||
| library: 'leancode_forms', |
There was a problem hiding this comment.
Why revert the name :D
| FocusNode? _focusNode; | ||
|
|
||
| /// The [FocusNode] bound to this field, created on first use. | ||
| /// | ||
| /// Throws a [StateError] if controller is disposed. | ||
| FocusNode get focusNode { | ||
| if (isDisposed) { | ||
| throw StateError( | ||
| 'Cannot use the focusNode of a disposed AdvancedTextFieldController.', | ||
| ); | ||
| } | ||
|
|
||
| return _focusNode ??= FocusNode( | ||
| debugLabel: | ||
| 'AdvancedTextFieldController${name?.isNotEmpty ?? false ? '($name)' : ''}', | ||
| ); | ||
| } | ||
|
|
There was a problem hiding this comment.
I guess it's removed because AdvancedFieldController already provides it, but now I think that we should be able to pass our own focus node. Add it to constructor
| this.validationError, | ||
| this.asyncError, | ||
| this.autovalidate = false, | ||
| this.mode = ValidationMode.disabled, |
There was a problem hiding this comment.
IIRC we wanted to change the name from disabled to manual
| // Runs async validation: starts a pass, waits for the result, and writes it | ||
| // only if still current. Round tracking lives on the controller (`_currentRound`, | ||
| // `_hasVerdict`, `_lastFailure`). | ||
| extension _ValidationRounds<T, E extends Object> |
There was a problem hiding this comment.
Let's do it as mixin if possible, so it's explicitly declared where these methods are coming from when reading advanced_field_controller.dart code
| part of 'advanced_form_controller.dart'; | ||
|
|
||
| // Listeners on child fields and subforms, and what to update when they change. | ||
| mixin _ChildWiring on ChangeNotifier { |
There was a problem hiding this comment.
As I said in the other comment, as long as it's mixin, not extension it's findable, then it's fine
| part of 'advanced_form_controller.dart'; | ||
|
|
||
| // Listeners on child fields and subforms, and what to update when they change. | ||
| mixin _ChildWiring on ChangeNotifier { |
There was a problem hiding this comment.
Splitting this logic into few mixins was a very good idea. Much more readable when logic is grouped
| @@ -0,0 +1,101 @@ | |||
| part of 'advanced_form_controller.dart'; | |||
There was a problem hiding this comment.
I doesn't have to be part of this file
There was a problem hiding this comment.
It's public API, just export it separately
| AdvancedFieldController({ | ||
| required T initialValue, | ||
| Validator<T, E>? validator, | ||
| AsyncValidation<T, E>? asyncValidation, | ||
| this.name, | ||
| }) : _value = AdvancedFieldState<T, E>(value: initialValue), |
There was a problem hiding this comment.
Oof I didn't notice it earlier. We have to allow developers to pass their own FocusNodes
| final bool autovalidate; | ||
| /// When this field validates itself. The **effective** mode: a field whose | ||
| /// form has validation switched off reports [ValidationMode.disabled]. | ||
| final ValidationMode mode; |
There was a problem hiding this comment.
I'd name this field validationMode
| /// Not used for identity — fields are identified by reference. | ||
| String? get name; | ||
|
|
||
| /// Tells the field the user has left it. |
There was a problem hiding this comment.
the user has left it
IDK, but sounds weird to me
|
|
||
| // Listeners on fields this form does not own — for external callbacks only. | ||
| // Does not update the form's own state. | ||
| mixin _Relations on ChangeNotifier { |
There was a problem hiding this comment.
Even though it's private, maybe we can tighten the on type to the controller type, instead of allowing any ChangeNotifier?
The similar thing goes for other mixins introduced in this PR
| unawaited( | ||
| validate().catchError((Object error, StackTrace stackTrace) { | ||
| _report(name, 'validating after focus loss', error, stackTrace); | ||
| return false; | ||
| }), | ||
| ); |
There was a problem hiding this comment.
I'd override handleUnfocus as async and use regular await + try/catch
|
|
||
| void _setState(AdvancedFormState newValue); | ||
|
|
||
| final _onValuesChanged = ChangeNotifier(); |
There was a problem hiding this comment.
Is this notifier (and _onStatusChanged) disposed of properly? 🤔
There was a problem hiding this comment.
Now at 1763 lines, tl;dr — split up into multiple files mayhaps?
No description provided.