feat(validation): cancellable, status-aware async validation on Angular 21 - #55
Merged
Merged
Conversation
vannt-dev
force-pushed
the
feat/async-validation-status-and-angular-21
branch
from
September 3, 2026 18:29
a76787f to
c58e288
Compare
Angular 19 -> 21 across the package, the demo app and the root, plus TypeScript 5.9.3, zone.js 0.16, @testing-library/jest-dom 7 and @types/node 26. Three things had to change for the bump to actually work. Angular 21 ships every entry point through `exports` alone, with no `main` or `types`. `tsconfig.base.json` still asks for node10 resolution, which cannot read an `exports` map, so the Angular package stopped resolving its own dependencies: `tsc -p packages/angular` reported 16 x TS2307, and every spec died in `test/setup.ts` on `@angular/core/testing`. Both the Angular package tsconfig and the demo app's now use `moduleResolution: "Bundler"`, which is what the Angular CLI itself uses - the demo app failed the same way, on `@angular/common/http` and `@angular/core/primitives/di`. ng-packagr 21 emits the type declarations as `dist/types/<name>.d.ts` instead of `dist/index.d.ts`, so the manifest's `types` pointed at a file the build no longer produces - the package would have published with types that resolve to nothing. `types` and `exports` now name the file that actually ships. `scripts/verify-package-entrypoints.js` fails the build on any manifest path that the build does not emit, so this cannot come back quietly; it runs in the quality-gates verify job next to the other checks. The lockfile was regenerated with npm 10 (npm 11 prunes other platforms' optional binaries on Windows). The previous one had ten entries under packages/angular/node_modules with no `resolved`/`integrity` at all and no entry for `injection-js`, which ng-packagr 21 requires - `npm ci` refused it outright and the Angular build died on the missing module. Every platform binary the Linux runners need is still present. The two `vitest.config.ts` files that Vite warned about are renamed to `.mts`; the contents are unchanged and nothing referenced them by path.
`ValidationResult` gains `complete` and `status`. Combining `valid` with `pending` was the only way to tell "nothing is wrong" from "nothing is wrong yet", and it reads as a green light either way, so a form with a remote rule still in flight looked valid. `status` is the single answer. `FieldDescription.validationMode: 'async'` declares a validator that returns a Promise without the `async` keyword, which detection cannot see. Declaring it keeps the sync pass from invoking the validator at all, and silences the dev warning, which exists to catch the accidental case. `validateFieldsAsync` takes a `ValidationContext`, forwards its `AbortSignal` to every validator, runs independent validators in parallel, skips validators once the signal is aborted, and reports an aborted run as incomplete. A validator that honours the signal the conventional way - by rejecting with an `AbortError` - no longer rejects the caller's `handleSubmit`; an error that is not an abort still propagates. Every adapter exposes `isValidating`, `isValidationComplete` and `validationStatus`, and applies latest-run-wins so a stale result cannot overwrite a newer one. A submit is not collateral damage of that: it validates the snapshot the user submitted under a controller of its own. Before this, typing while a submit was in flight cancelled it outright - no `onValid`, no `onInvalid`, no `isSubmitted`, just a button that re-enabled itself. `touchAll()` expands to the leaf paths that exist in the data (`contacts[0].email`, not `contacts`) through the new `collectFieldPaths`, which skips what validation skips - fields hidden by `appearCondition` and disabled ones. Group items receive `touched` and report blur with their full path, so "show the error once the field is touched" works inside a repeatable group. An item with no touched keys still receives a map: handing it `undefined` flipped the nested input into tracking touched by itself, which then survived the owner clearing the map. `indexGroupPathMap` indexes those maps by item and is exported for custom renderers. React seeds `isValid` from the initial data rather than from an effect. Effects do not run on the server, so a server-rendered form shipped `isValid: true` for an empty required field and never corrected it. React also stops handing the whole touched map to every field - only repeatable groups read it, and passing it everywhere re-rendered every field on each blur - and its `isValidationComplete` now matches Vue and Angular. Angular's per-item error and touched maps return a shared frozen object rather than a fresh literal, which was a new binding identity on every change detection pass.
vannt-dev
force-pushed
the
feat/async-validation-status-and-angular-21
branch
from
September 3, 2026 18:36
c58e288 to
349eeeb
Compare
The docs still described `ValidationResult` as `{ valid, errors }` and said
nothing about what the adapters now expose, so a reader had no way to know
that `valid` alone is not the answer.
Core: documents the full result shape, why `status` is the member to read,
`validationMode: 'async'` for a validator that returns a Promise without the
keyword, the `ValidationContext`/`AbortSignal` argument with a fetch example,
and that independent validators run in parallel. The export list gains
`collectFieldPaths`, `indexGroupPathMap` and `ValidationContext`.
React, Vue and Angular: their state tables gain `isValidating`,
`isValidationComplete` and `validationStatus`, and the paragraph about live
validation now says what actually happens - latest-run-wins, typing cancels a
live run, a submit is not cancelled by typing.
Root README: the `validate` signature includes the context argument, there is
a `validationMode` row beside it, the validation example reads `status`, and
the touched section says the map is keyed by full path, so it reaches inside
repeatable groups and skips fields validation itself skips.
The UI-kit recipes gain a table of which member to bind for what, and the
Angular README replaces its "(Angular 19+)" heading with the range the package
actually declares and the version CI exercises.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three commits, meant to be read in order.
1.
chore(deps): upgrade to Angular 21 and TypeScript 5.9Angular 19 → 21 across the package, the demo app and the root, plus TypeScript 5.9.3, zone.js 0.16, jest-dom 7 and @types/node 26. Three things blocked the bump:
exportsalone, andtsconfig.base.jsonasks for node10 resolution, which cannot read anexportsmap.tsc -p packages/angularreported 16 × TS2307 and all 11 spec files died intest/setup.ts. The Angular tsconfig now usesmoduleResolution: "Bundler".dist/types/<name>.d.ts, notdist/index.d.ts, so the manifest'stypespointed at a file the build no longer produces — the package would have published with types resolving to nothing. Fixed, andscripts/verify-package-entrypoints.jsnow fails the build on any manifest path the build does not emit (wired into the quality-gates verify job).packages/angular/node_moduleshad noresolved/integrity, and there was no entry forinjection-js, which ng-packagr 21 requires.npm cirefused it and the Angular build died on the missing module. Regenerated with npm 10 (npm 11 prunes other platforms' optional binaries on Windows); every platform binary the Linux runners need is still present.The two
vitest.config.tsfiles Vite warned about are renamed to.mts, contents unchanged.2.
feat(validation): make async validation cancellable and status-awareValidationResultgainscompleteandstatus('valid' | 'invalid' | 'pending').validalongsidependingread as a green light while remote rules were still in flight.FieldDescription.validationMode: 'async'declares a Promise-returning validator that detection cannot see, keeps the sync pass from invoking it, and silences the dev warning (which exists for the accidental case).validateFieldsAsyncforwards anAbortSignalto validators, runs independent ones in parallel, skips them once aborted, and reports an aborted run as incomplete. A validator that honours the signal by rejecting withAbortErrorno longer rejects the caller'shandleSubmit; other errors still propagate.isValidating/isValidationComplete/validationStatuswith latest-run-wins. A submit now survives typing: previously, editing a field mid-submit cancelled it outright — noonValid, noonInvalid, noisSubmitted, just a button that re-enabled itself.touchAll()expands to real leaf paths (contacts[0].email) viacollectFieldPaths, skipping what validation skips. Group items receivetouchedand report blur with their full path. An item with no touched keys still receives a map — handing itundefinedflipped the nested input into tracking touched itself, which survived the owner clearing the map.isValidis seeded from the initial data, not an effect. Effects do not run on the server, so a server-rendered form shippedisValid: truefor an empty required field and never corrected it.Changeset included: minor on all four packages.
3.
docs: bring the READMEs in line with the validation APIThe docs still described
ValidationResultas{ valid, errors }and said nothing about what the adapters now expose, so a reader had no way to know thatvalidalone is not the answer.statusis the member to read,validationMode: 'async', and theValidationContext/AbortSignalargument with a fetch example. Export list gainscollectFieldPaths,indexGroupPathMap,ValidationContext.isValidating,isValidationComplete,validationStatus; the live-validation paragraph now states latest-run-wins and that a submit is not cancelled by typing.validatesignature includes the context argument, avalidationModerow sits beside it, the example readsstatus, and the touched section explains full-path keying inside repeatable groups.(Angular 19+)heading is replaced by the range the package declares (>=14 <22) and the version CI exercises (21).Verification
npm citest:typestest:scriptsmoduleResolutionfix)Every behaviour fix landed test-first; the Vue and Angular submit fixes were re-verified by temporarily reverting the guard to confirm the new tests fail without it.