VeryGoodOpenSource / VeryGoodOpenSource/formz
feat: support externally-forced validation errors
Nobody has claimed this yet.
- Dominant language
- Dart
- Stars
- 487
- Forks
- 42
- Avg merge
- 4d 9h
- Merged PRs (30d)
- 2
Description
Description
A FormzInput can only ever surface an error that its own validator produced — error is defined as validator(value) (lib/formz.dart:104). That leaves nowhere to put an error that originates outside the input:
- a server rejection, e.g. "this email is already taken"
- a cross-field rule, e.g. a password confirmation that does not match
- the result of an async check
Today the workaround is to hold that error in a separate field on the form state and reconcile it with displayError by hand at the widget layer, which means isValid lies: the form reports itself valid while the UI is showing an error.
Flutter hit the same problem and solved it in 3.24 with FormField.forceErrorText (flutter/flutter#132903), which puts a field into an error state without running its validator.
This came out of the documentation review for #122. That issue is about FormState.validateGranularly() (flutter/flutter#135578) and is already handled by #146 — this is the one remaining Flutter form API since 3.22 that has no formz equivalent and would need new library surface. Everything else Flutter has added (AutovalidateMode.onUnfocus, AutovalidateMode.onUserInteractionIfError, FormField.onReset, FormField.errorBuilder, FormState.clearError()) is widget-layer behavior that does not belong in a pure-Dart package.
Requirements
- All CI/CD checks are passing.
- There is no drop in the test coverage percentage.
- A forced error takes precedence over
validator(value). -
isValid,isNotValid,error, anddisplayErrorall account for a forced error. - The behavior is opt-in and non-breaking for existing
FormzInputsubclasses. -
Formz.validateandFormzMixin.isValidreflect forced errors, since they delegate toisValid. - The README gains a section covering it.
- Unit tests cover a forced error on both pure and dirty inputs, and its interaction with
FormzInputErrorCacheMixin.
Additional Context
Proposed design: a mixin
This follows the FormzInputErrorCacheMixin precedent (lib/formz.dart:136-144) — opt-in, const-friendly, and no impact on FormzInput's core.
/// Mixin for [FormzInput] that allows an error to be supplied from outside the
/// input, such as from a server response or a cross-field rule.
///
/// The [forcedError] takes precedence over the result of [validator].
mixin FormzInputForcedErrorMixin<T, E> on FormzInput<T, E> {
/// An error supplied from outside the input.
E? get forcedError;
@override
E? get error => forcedError ?? super.error;
@override
bool get isValid => error == null;
@override
E? get displayError => forcedError ?? super.displayError;
}
Usage — the subclass declares forcedError and forwards it through a const constructor, the same way it already forwards value:
enum EmailError { empty, invalid, alreadyTaken }
class Email extends FormzInput<String, EmailError>
with FormzInputForcedErrorMixin<String, EmailError> {
const Email.pure({String value = '', this.forcedError}) : super.pure(value);
const Email.dirty({String value = '', this.forcedError}) : super.dirty(value);
@override
final EmailError? forcedError;
@override
EmailError? validator(String value) {
return value.isEmpty ? EmailError.empty : null;
}
}
// After the server rejects the submission:
state = state.copyWith(
email: Email.dirty(
value: state.email.value,
forcedError: EmailError.alreadyTaken,
),
);
Alternative considered: an optional named parameter
Adding {E? forcedError} to FormzInput.pure and FormzInput.dirty is closer to Flutter's shape, but every subclass still has to forward the parameter to expose it, and it pulls forcedError into hashCode, operator ==, and toString (lib/formz.dart:114-130) for everyone, whether they use it or not.
Open question to settle before implementation
Should a forced error surface through displayError on a pure input?
Flutter's forceErrorText shows regardless of user interaction, which argues yes — and the snippet above reflects that. But formz's displayError contract is currently "only once the input is dirty" (lib/formz.dart:108), so the two readings conflict. Worth deciding deliberately rather than falling out of the implementation.
Related
- #15 —
Futurevalidators. A forced error is a natural place to land an async result. - #121 — form-level validation rules. Cross-field errors need somewhere to attach.
- #86 — fields that do not require validation.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in lib/formz.dart around FormzInput, FormzInputErrorCacheMixin, and the existing error and displayError accessors. Resolve whether forced errors appear on pure inputs, then cover pure and dirty inputs, cache interaction, validity aggregation, and the README; done means the listed requirements pass without breaking existing subclasses.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- dart
- Domain
- documentation, testing, tooling
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100