valiform 2.1.1
valiform: ^2.1.1 copied to clipboard
A Flutter form validation library built on Validart. Provides reactive state management, typed fields, and seamless Form widget integration.
Changelog #
2.1.1 - 2026-04-28 #
Fixed #
VForm.object'svalidate()/silentValidate()no longer throwTypeErroron partial / empty forms. Previously, both methods invoked the user-suppliedbuilderagainst the raw field snapshot before running validation, which crashed whenever a builder dereferenceddata['key']into a non-nullable parameter while another field was still null (the canonical Dart factory pattern, e.g.User(name: data['name'], email: data['email'])without?? fallback). The silent validators now wrap the canonicalobject.safeParse(builder(raw))path in aTypeErrorcatch and fall back to per-field iteration overobject.schemawith raw values when the builder cannot constructT. Schema-level rules (refine,refineField,refineFieldRaw,equalFields,whenRules, container preprocess) still run on the canonical path whenever the builder succeeds, so existing schemas don't regress; partial forms now surface per-field "required" / type errors as expected instead of crashing.- Container preprocess closures on
VForm.object(object.hasPreprocessors) likewise tolerate partial snapshots: whenbuilder(snapshot)throws, preprocess is skipped for that tick and the snapshot passes through unchanged. Once every field is filled in, the canonical pipeline reapplies preprocess on the next validation cycle. - The fallback path re-throws the underlying
TypeErrorwhen per-field validation passes for every field but the builder still cannot constructT— that combination can only mean a schema/class mismatch (e.g. a field declared.nullable()on the schema but non-nullable on the user's class). Surfacing the original error keeps such mismatches loud instead of silently reportingsilentValidate() == falsewith an emptyerrors()map.
2.1.0 - 2026-04-27 #
Fixed #
onValueChangedno longer throws on incompleteVForm.objectforms. The form-levelonValueChangedcallback (andaddValueChangedListener/removeValueChangedListener) now always deliversMap<String, dynamic>(raw field values), regardless of whether the form was created from aVMapor aVObject<T>. Previously,VForm.object<T>invoked the user-suppliedbuilderagainst the partial field map on every field change, which threwTypeErrorwhenever the builder dereferenceddata['key']into a non-nullable parameter while other fields were still empty (the idiomatic Dart factory pattern). A field change does not imply the form is valid enough to constructT; consumers needing the typed value should callform.value/form.valueAsyncafterform.validate()returnstrue, or guard thebuilderwith??defaults / nullable parameters.- The previous async-form no-op restriction on value-changed listeners is removed —
rawValueis sync-safe regardless of the schema's async pipeline, so the same callback shape now fires on sync and async forms alike.
2.0.0 - 2026-04-25 #
Breaking Changes #
-
Removed
VMap.refineFormFieldextension and the supportingExpando/VFieldValidatorclass. The valiform-specific extension predates validart'srefineFieldand had subtly different semantics —refineFormFieldcallback receivedform.rawValue(pre-pipeline) for the per-field channel but the parsed map forsilentValidate, leading to divergence on schemas with field-level transforms. The two semantics are now provided cleanly by validart 2.0.0:V.map({...}).refineField(check, path: 'x')→ callback receives the parsed map (transforms applied). Recommended for the vast majority of cases.V.map({...}).refineFieldRaw(check, path: 'x')→ callback receives the raw input (post container preprocess + type check, before per-field iteration). Reach for it when the rule depends on the original input shape (case, whitespace, pre-coercion).
Both surface inline under the target field thanks to the schema-error demux. Migration:
V.map({...}).refineFormField(check, path: 'x', message: '...')→V.map({...}).refineField(check, path: 'x', message: '...')— same result for callbacks that don't depend on case/whitespace.- If your callback compared raw values that get transformed by the field-level pipeline (e.g.
data['email']against an expected literal when the field has.toLowerCase()), userefineFieldRawinstead.
See updated
example/lib/pages/password_match_page.dart,complex_form_page.dart, and the newrefine_field_raw_page.dartfor migration patterns.
Required #
- Bumped minimum
validartconstraint to2.0.0. New upstream APIs consumed:VFailure.rootMessages()— root-level error extraction for the new banner channel.refine(..., dependsOn: {...})onVMap/VObjectfor cross-field error aggregation.- Fluent
V.object<T>().field(...)API (theconfigure:callback no longer exists upstream). - New
VObjectcombinators:equalFields,when,refineField,refineFieldRaw,pick,omit,merge,array. VType.runPreprocessors/runPreprocessorsAsync/hasPreprocessors— public accessors used by the container-preprocess routing fix below.VType.addRaw— low-level hook backingrefineFieldRaw.
Added #
VForm.rootErrors/VForm.rootErrorsAsync— surface form-level errors emitted by schema-levelrefine()rules with no specific field path (date-range checks, totals, etc.). Self-contained getter: each access re-runs the schema-levelsafeParseagainst the current parsed values, so aListenableBuilderonform.listenablekeeps a banner in sync without a manualsilentValidate()call. The async variant awaits each field's async pipeline before re-runningsafeParseAsyncto catchrefineAsync(..., dependsOn:)failures. Sync getter throwsVAsyncRequiredExceptionon async schemas — userootErrorsAsync.example/lib/pages/object_validation_page.dart— three sections demoing the newVObjectcombinators on typed DTOs:equalFields(password match),when(US tax ID conditional),refineField(date-range with error pinned to a single field).example/lib/pages/root_errors_page.dart—refine(..., dependsOn:)with a banner renderingform.rootErrorsalongside field-keyed errors. Demonstrates the aggregation introduced in validart 2.0.0 (whendependsOnis declared, the cross-field rule keeps running even when sibling fields fail individually).example/lib/pages/refine_field_raw_page.dart— interactive demo showingrefineFieldandrefineFieldRawside-by-side with the same predicate (codelength must be 8) and a.trim().toUpperCase()transform on the field. Same input → different verdicts. Use it as a template when deciding which API to reach for.- New test groups in
test/src/form_test.dartpinning the schema-error demux surface for bothrefineFieldandrefineFieldRaw(path-keyed error reachesfield.error/form.errors()/field.validator).
Fixed #
VForm.objectnow propagatesVObject.whenRulesinto per-field validators, matching the behaviour already in place forVForm.map. Previously, conditional rules declared on aVObjectvia.when(...)were silently ignored by the form layer (only the schema-levelvalidate(builder(raw))saw them, but per-fieldFormField.validatordid not), soform.validate()could returntruefor inputs the schema knew to be invalid. Pinned by a regression test in theConditional validation (when)group.form.validate()andform.validateAsync()now include schema-level rules in their return value. Previously they delegated only toFormState.validate()(per-fieldFormField.validator), so a schema withrefine(..., dependsOn: {...})orequalFields(...)could fail at the schema level whilevalidate()still returnedtrue— the consumer would thensubmit()data the schema rejected. The fix re-runs the silent schema validator after the per-field pass and returnsfieldsValid && schemaValid. Behavioural consequence: code that previously had to writeif (form.validate() && form.silentValidate())as a workaround can now drop the second clause; code that wasn't aware of the gap will start receivingfalsecorrectly when a root rule fails. Bundled cleanup ofexample/lib/pages/{root_errors,password_match,object_validation}_page.dartremoves the workaround. Pinned by widget tests in theForm-level (root) errorsgroup.- Schema-level errors with a non-empty path now surface inline under the target field. Errors emitted by
VMap.refineField(check, path: 'x'),VObject<T>.refineField(check, path: 'x'), or any nested-path schema construct previously landed only in thesafeParsefailure —field.error,form.errors(), andFormField.validatordid not see them, so the UI showed no error even thoughsilentValidate()returnedfalse.VFormnow demuxes theVFailureafter everyvalidate*/silentValidate*call: errors with a top-level path go to the correspondingVField(via aschemaErrorLookupclosure consulted by_runValidators), errors with an empty path go toform.rootErrors. The lookup is on-demand for sync forms (re-runs the schema each time so it always reflects the current sibling values — O(N) per keystroke for schemas that actually have schema-level rules; pure schemas pay no cost). Async forms use a snapshot refreshed byvalidateAsync/silentValidateAsync. - Container
preprocess()now reaches the per-field validator, mirroring the order inVMap.safeParse/VObject.safeParse:container preprocess → field preprocess → field validators → field transforms. Previously, container preprocess only ran insidesafeParsedirect (the schema-level path), while eachVFieldknew only its own_typeand ran its pipeline without any awareness of the parent. Result:form.validator(value)andform.silentValidate()could disagree, with the UI displaying an error that the schema had already "fixed" via preprocess. Now both paths agree. The canonical use case is cross-field rewrites — e.g.V.map({...}).preprocess((m) => m['country'] == 'US' ? {...m, 'state': m['state'].toUpperCase()} : m)— which no per-field transform can express because a field doesn't see its siblings. Limit: when the container has onlypreprocessAsync, the syncfield.validatorcontinues to fall through (it cannot await) — useform.validateAsyncfor the full async pipeline. Implementation: eachVFieldreceives a snapshot-and-preprocess closure that mounts the full sibling map, runscontainer.runPreprocessors, and reads back the field's slot. Schemas without container preprocess (the common case) pay zero overhead — gated onVType.hasPreprocessors.
Changed #
example/lib/pages/object_form_page.dartmigrated to validart 2.0.0's fluentV.object<T>().field(...)API. Theconfigure:callback no longer exists upstream — see the validart 2.0.0 changelog for the migration.
Notes #
- All other behavior from valiform 1.x stays compatible.
VForm,VField,attachController,attachTextController,validate/validateAsyncsemantics — all unchanged outside of the bullets above. - Internal:
VForm's silent validators changed signature frombool Function(...)/Future<bool> Function(...)to(bool, List<String>, Map<String, String>) Function(...)/Future<(bool, List<String>, Map<String, String>)> Function(...)so they can carry the validity flag, root messages, and the schema field-error demux in a single pass. Public API unchanged — only relevant if you were extendingVFormdirectly (which is unusual;VFormisfinalin spirit). - Internal:
_runWithRoot(the schema-error demux helper) delegates root-message extraction toVFailure.rootMessages()instead of duplicating the loop. Field-message demux stays custom because it routes by top-level segment (path.first) to matchVForm._fields's top-level indexing —VFailure.toMapFirst()keys bypathString(joined with.) which would mis-route nested errors.
1.2.0 - 2026-04-23 #
Changed #
- Bumped minimum
validartconstraint to1.3.0for the domain-prefixed error-code format (string.email,number.positive,int.even, ...) introduced in validart 1.2.0 and theV.coerce.date()multi-format parsing added in 1.3.0. No API surface of valiform itself changed — the genericVForm<T>/VField<T>contract stays identical. - Example —
locale_page.dartPT-BR map migrated to the nested type-prefixed shape. Every validator's translation now lives under its owning type group ('string': { 'email': '...' },'int': { 'even': '...' },'enum': { 'invalid': '...' }, ...). The legacy flat keys ('invalid_email','positive','even','weekday','invalid_enum', ...) were emitted by validart ≤ 1.1.0 but no longer match any code emitted by 1.2.0+, so the previous map silently fell back to English for most validators.required,invalid_typeandcustomstay flat as documented global fallbacks.
1.1.0 - 2026-04-22 #
Added #
- Async validation — first-class support for validart 1.1.0 async primitives (
refineAsync,preprocessAsync,transformAsync).VForm.validateAsync()— runs the full async pipeline and surfaces errors through the normalFormFielderror channel (via persistent imperative errors), so the UI updates exactly like sync validation.VForm.silentValidateAsync()— async validation without touching the UI.VForm.errorsAsync()/VForm.vErrorsAsync()— async counterparts for inspecting current errors.VForm.valueAsync— awaits each field's async pipeline and returns the fully-parsed value.VForm.hasAsync/VField.hasAsync— introspection flag for schemas that require async validation.VField.validateAsync(),VField.errorAsync,VField.vErrorAsync,VField.parsedValueAsync— per-field async APIs with the same semantics as their sync siblings.- Conditional
.when()rules now detect async schemas and register them into the async validation path automatically.
- Async example page —
example/lib/pages/async_validation_page.dartdemonstrates a simulated remote username-availability check with loading state. - Schema
defaultValuenow seeds the VField initial value — when.form()isn't given an explicit value for a field, the schema'sdefaultValue(if any) auto-populates the UI and is the target ofreset(). Resolution order:initialValues[key](even an explicitnull) →schema.defaultValue→null. Reinforces the semantic that a field withdefaultValueis never required — validart substitutes the default for null before any validator runs. Required Messageexample page —example/lib/pages/required_message_page.dartcomparesV.bool(message: ...)againstpreprocess((v) => v ?? false)as two ways to customize the error on an untouched checkbox.Default Valueexample page —example/lib/pages/default_value_page.dartdemonstrates the three combinations (defaultValueonly,initialValuesonly, both) side by side with their respectivereset()andrequiredsemantics.VField.initialValuegetter — exposes the stable initial value resolved at form construction (initialValues[key]→schema.defaultValue→null). Prefer binding widgets'initialValueparameter to this getter instead ofVField.valuesoFormField.reset()always targets the true starting point, even when the widget tree rebuilds during typing.
Changed #
- Synchronous inspection methods now mirror validart's strict contract: when the schema contains any async step,
VForm.validate,silentValidate,errors,vErrors,value,VField.validate,error,vError, andparsedValuethrowVAsyncRequiredException. Use the corresponding*Asyncvariants.VField.validator(T?)is the single sync adapter kept tolerant — it is required by Flutter's synchronousFormField.validatorsignature and is the channelvalidateAsyncuses to surface async errors viasetError(persist: true). - Bumped minimum validart constraint to
1.1.0forVType.isNullable,VType.hasAsync,VType.hasDefault,VType.defaultValueOrNull, and the async pipeline APIs.
Fixed #
VField.parsedValue/parsedValueAsyncnow normalize empty strings tonullbefore callingsafeParse, so a schema'sdefaultValueis applied consistently when the user clears a text field. PreviouslyparsedValuereturned the raw empty string, causingform.valueto disagree withform.validate()in defaulted schemas.
1.0.0 - 2026-04-18 #
Breaking Changes #
- Removed validart re-export — import
package:validart/validart.dartseparately. - Removed built-in
TextEditingController— useattachTextController()for bidirectional sync. VFormis now generic —VForm<Map<String, dynamic>>for VMap,VForm<T>for VObject.- Replaced
Validart()instance — use theVclass (V.string(),V.map(), ...). - Replaced
.refine(check, path:)— use.refineFormField(check, path:). - Removed
VNumsupport — useVIntorVDoubledirectly. - Renamed
defaultValues→initialValuesonVMap.form()/VForm.map(). - Renamed
defaultValue→initialValueonVObject.form()/VForm.object(). VField.onChangednow acceptsT?— compatible with widgets likeDropdownButtonFormFieldwhoseonChangedpasses a nullable value.VField.validate()now runs all validators without side-effects — previously only ran the schema check. Behaviour of the Flutter-facingvalidator(value)is unchanged.
Added #
- VObject support —
V.object<T>().form(builder:)returns typedTinstead ofMap. - Typed fields via
mapType—VField<Country>,VField<DateTime>, and any custom type are preserved across the schema pipeline (no moreVField<dynamic>fallback). - Conditional validation (
.when()) — schema rules are plumbed into per-field validators automatically. - Nullable fields — empty strings are normalized to
nullfor.nullable()types. initialValues/initialValue— seed values at form construction, forVMapandVObjectforms respectively.parsedValuegetter onVField— value after pipeline transforms (trim,toLowerCase, ...).rawValueonVForm— untransformed field values asMap<String, dynamic>.onValueChangedon form factory plusaddValueChangedListener/removeValueChangedListeneronVFormfor dynamic subscription.attachController(ValueNotifier<T?>, {owns = true})— bidirectional sync with any custom controller that extendsValueNotifier<T?>(e.g.LuneSelectFieldController<T>). Owned by default — the controller is disposed together with the field. Passowns: falseto keep external lifecycle management.attachTextController(TextEditingController, {owns = true})— extension onVField<String>for text inputs (sinceTextEditingController.valueisTextEditingValue, notString).controller/textControllergetters — typed access to the currently attached controller.detachController()— remove sync listeners without disposing the controller.onValueChanged(callback)onVField— bridge for external state that isn't aValueNotifier. Registers a typed callback on every value change and returns a dispose function.- Imperative error setting —
VField.setError(message, {persist, force})/clearError()and theVFormcounterparts (setError,setErrors,clearError,clearErrors). Useful for backend rejections, async checks, and external-state business rules.- Default (one-shot) — the error surfaces on the next
validator()call and is cleared on it, even when a standard validator wins the precedence. Prevents "ghost" errors on later valid input. persist: true— keeps the error across validations untilclearError()is called explicitly.force: true— overrides standard-validator precedence so the manual error shows even on fields that would fail their own rules.persist: true, force: true— always shown until cleared (server-side blocks like "Account suspended").
- Default (one-shot) — the error surfaces on the next
VField.key— optionalGlobalKey<FormFieldState<T>>that, when attached to aFormField, letssetErrorrevalidate only that field (other fields are untouched).VForm.errors()/VField.error— read-only inspection of current validation state (all fields or single field). Does NOT consume one-shot manual errors, does NOT touch the UI. Perfect for live error summary panels and debug tooling.VForm.vErrors()/VField.vError— likeerrors()/errorbut return the fullList<VError>(preservingcode,path,message). Useful for array fields wherepathcontains the failing element's index, or when you need the validator code for custom UI.
Changed #
form.valuereturns parsed values (transforms applied).form.silentValidate()now mirrorsvalidate()semantically — runs per-field validators AND the schema validator, consuming one-shot manual errors — while still not touching the UI.- Controller sync uses equality checks and a
_syncingguard to avoid cascading updates.
Fixed #
- Re-attaching the same owned controller no longer disposes it —
attachControllerandattachTextControlleruse an identity check before disposing the previously owned instance, sofield.attachController(ctrl); field.attachController(ctrl);is safe.
0.0.3 - 2025-02-27 #
0.0.2 - 2025-02-27 #
0.0.1 - 2025-02-25 #
- Initial release