@dynamic-field-kit/angular 1.5.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,171 @@
1
1
  # @dynamic-field-kit/angular
2
2
 
3
+ ## 1.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Cancellable, status-aware async validation; touched state that reaches inside repeatable groups; Angular 21 and TypeScript 5.9 support; and peer ranges corrected to the versions that actually work, proven at both ends in CI.
8
+ - 67e4eec: Give every adapter one renderer-prop contract, unique field ids, and a touched
9
+ state the form store can actually drive.
10
+
11
+ Five things went wrong at once for anyone building a real form on 1.5.1, and
12
+ four of them share a cause: `FieldRendererProps` was a type nobody enforced.
13
+ Each adapter hand-wrote the object it handed the registered renderer, and the
14
+ three lists drifted. React dropped `placeholder`, `min`, `max`, `step`,
15
+ `accept` and `multiple`. Vue dropped `required`, `id`, `dirty` and the aria
16
+ flags. Angular dropped `touched`, `dirty` and `id` — so an Angular renderer had
17
+ no way to know whether a field had been touched, and "only show the error once
18
+ the user leaves the field" had to be rebuilt by hand. Setting
19
+ `placeholder` on a `FieldDescription` therefore did nothing at all on React and
20
+ Vue: no error, no warning, the value simply vanished. Core now owns the list as
21
+ `FIELD_RENDERER_PROP_KEYS` and builds the bag once in
22
+ `buildFieldRendererProps`, which all three adapters call, and
23
+ `scripts/check-renderer-prop-parity.js` fails the build if an adapter stops
24
+ forwarding one. The single deliberate deviation is Vue's `class` in place of
25
+ `className`: forwarding `className` lets it fall through to a renderer's root
26
+ element, where Vue assigns `el.className` and wipes the class the renderer set
27
+ on itself.
28
+
29
+ Field ids were `dfk-field-${name}`, derived from the field name alone. Two
30
+ forms holding a field of the same name — a create form beside an edit form, the
31
+ most ordinary layout there is — emitted the same DOM id twice, which is invalid
32
+ HTML and leaves every `label[for]` pointing at two inputs. Ids are now
33
+ namespaced per `MultiFieldInput` instance (React `useId`, so it is SSR-safe;
34
+ Vue's instance uid; a counter on Angular). Set `idPrefix` to pin them —
35
+ `idPrefix="dfk-field"` reproduces the old ids exactly — or give a single field
36
+ its own id with the new `FieldDescription.id`.
37
+
38
+ Touched state had two independent trackers that never met: the one in
39
+ `useDynamicForm`, and a private one inside `MultiFieldInput` that only blur
40
+ could set and that was the one renderers actually saw. So
41
+ `setFieldTouched` in an `onInvalid` handler changed nothing visible, submitting
42
+ a form nobody had focused showed no errors at all (the button looked broken),
43
+ and `reset()` could not clear the touched state a previous submit had left
44
+ behind. `MultiFieldInput` now accepts `touched` as a controlled prop —
45
+ `useDynamicForm` becomes the single source of truth for it, exactly as
46
+ `properties`/`onChange` already were for data — plus `onTouchedChange`, and a
47
+ `form` shorthand (React and Vue) that wires data, change, blur and touched in
48
+ one prop. `handleSubmit` marks every field touched before validating, and the
49
+ new `touchAll()`/`resetTouched()` sit alongside it. Omit `touched` and the old
50
+ internal tracker still runs, so nothing breaks; for that mode a ref
51
+ (`resetTouched()` on React and Vue, a public method on Angular) can clear it
52
+ without remounting the component.
53
+
54
+ The only behaviour change to watch for is the generated ids. Anything pinned to
55
+ a literal `dfk-field-*` id in CSS or a test needs either `idPrefix="dfk-field"`
56
+ or a per-field `id`. Angular's `MultiFieldInput` also loses four undocumented
57
+ template helpers — `getResolvedOptions`, `getDisabled`, `getReadOnly` and
58
+ `getError` — which its own template no longer calls now that `FieldInput`
59
+ resolves all of it through core. Keeping a second copy of that logic beside the
60
+ shared one is how the adapters drifted apart to begin with; the equivalents are
61
+ `resolveOptions`, `resolveDisabled`, `resolveReadOnly` and `validateField`,
62
+ already re-exported from this package.
63
+
64
+ Form validity now reflects current data immediately instead of merely checking
65
+ the lazily populated `errors` map. The error map remains lazy for display, and
66
+ passing a form binding (or the new controlled `errors` input) makes that same
67
+ map the renderer's source of truth, removing the previous timing mismatch.
68
+
69
+ Promise-based validators are no longer silently accepted on submit.
70
+ `validateFields` reports unresolved field names in `pending`; every framework
71
+ form helper uses one async-capable validation pass before dispatching submit
72
+ callbacks. React, Vue and Angular also expose
73
+ `validateAsync()` for explicit pre-submit checks. Live `isValid` remains a
74
+ synchronous answer because a property/computed/signal cannot await.
75
+
76
+ The new UI-kit recipes show complete touched/error wiring for Ant Design,
77
+ Vuetify and Angular Material.
78
+
79
+ - 5e0b08f: Make async validation answerable: a status you can act on, runs that cancel
80
+ cleanly, and touched state that reaches inside repeatable groups.
81
+
82
+ `ValidationResult` gains `complete` and `status` (`'valid' | 'invalid' |
83
+ 'pending'`). Combining `valid` with `pending` was the only way to tell "nothing
84
+ is wrong" from "nothing is wrong _yet_", and everyone got it wrong the same
85
+ way — a `valid: true` with async rules still in flight reads as a green light.
86
+ `status` is the single answer; `complete` says whether every applicable
87
+ validator finished. Both are always present on a result the library returns, so
88
+ reading them needs no fallback; code that constructs a `ValidationResult` by
89
+ hand (a mock, a wrapper typed to return one) has to supply them.
90
+
91
+ `FieldDescription.validationMode: 'async'` declares a validator that returns a
92
+ Promise without the `async` keyword, which detection cannot see. Declaring it
93
+ keeps the synchronous pass from invoking the validator at all — and, unlike
94
+ detection, it is an explicit opt-in, so the dev warning about a field the live
95
+ pass cannot check stays quiet for it.
96
+
97
+ `validateFieldsAsync` now takes a `ValidationContext` and forwards its
98
+ `AbortSignal` to every validator, runs independent validators in parallel
99
+ instead of awaiting them one after another, skips validators once the signal is
100
+ aborted, and reports an aborted run as `complete: false` / `status: 'pending'`.
101
+ A validator that honours the signal the conventional way — rejecting with an
102
+ `AbortError` — no longer rejects the caller's `handleSubmit`; an error that is
103
+ not an abort still propagates.
104
+
105
+ Each adapter's form helper exposes `isValidating`, `isValidationComplete` and
106
+ `validationStatus`, and applies latest-run-wins: typing cancels an in-flight
107
+ live validation so a stale result cannot overwrite a newer one. A submit is not
108
+ collateral damage of that — it validates the snapshot the user submitted under
109
+ a controller of its own, so editing a field mid-flight no longer leaves the
110
+ form with the submit silently dropped, no `onValid`/`onInvalid`, and a button
111
+ that just re-enables.
112
+
113
+ `touchAll()` now expands to the concrete leaf paths that exist in the data
114
+ (`contacts[0].email`, not `contacts`) via the new `collectFieldPaths`, skipping
115
+ fields validation itself skips — hidden by `appearCondition`, or disabled.
116
+ Repeatable group items receive `touched` and report blur with their full path,
117
+ so a UI kit that only shows an error once a field is touched now works inside a
118
+ group. An item with no touched keys still receives a map rather than
119
+ `undefined`, which previously flipped the nested input into tracking touched by
120
+ itself and left it stale after the owner cleared the map. The new
121
+ `indexGroupPathMap` is what indexes those maps by item, exported so a custom
122
+ renderer can do the same without filtering the whole map per item.
123
+
124
+ React's `isValid` is now seeded from the initial data instead of from an
125
+ effect. An effect never runs on the server, so a server-rendered form shipped
126
+ `isValid: true` for an empty required field and never corrected it — a submit
127
+ button rendered enabled and stayed that way.
128
+
129
+ `@dynamic-field-kit/angular`'s `types` entry pointed at `dist/index.d.ts`,
130
+ which is not where its type declarations are emitted any more; it and the
131
+ `exports` block now point at the file that actually ships, so TypeScript
132
+ consumers resolve the package's types again.
133
+
134
+ - eec9386: Correct the peer ranges to the ones that actually work, and prove both ends of
135
+ each in CI.
136
+
137
+ `@dynamic-field-kit/angular` declared `@angular/core` and `@angular/common` as
138
+ `>=14 <22`, but the form store is built on `signal` and `computed`, which
139
+ Angular introduced in **16**. On 14 or 15 npm accepted the install and the
140
+ package then failed on import - the manifest promised something it could not
141
+ do. The range is now `>=16 <22`, so the same install is refused up front.
142
+
143
+ `@dynamic-field-kit/vue` moves from `vue ^3.0.0` to `^3.2.0`.
144
+ `useDynamicForm` now aborts an in-flight validation when the owning effect
145
+ scope is disposed, using `getCurrentScope` / `onScopeDispose` - both Vue 3.2.
146
+ Without this an unmounted form held its request open until the response came
147
+ back. If you are on Vue 3.0 or 3.1, stay on 1.5.x; nothing else in the package
148
+ ever required 3.2, but nothing tested below it either.
149
+
150
+ Both ranges are now verified rather than asserted:
151
+ `scripts/verify-vue-peer-range.js` server-renders the packed tarballs under Vue
152
+ 3.2 and the newest 3.x, and `scripts/verify-angular-peer-range.js` installs
153
+ them against Angular 16 and 21 and checks the package imports, its components
154
+ evaluate and it shares one registry with core. Both run in the CI verify job,
155
+ next to the React one that has existed since 1.5.0. A render is out of reach
156
+ for Angular - the published fesm2022 needs the CLI's linker to instantiate a
157
+ component - but import-and-wire is the level that breaks across majors, which
158
+ is exactly how a floor of 14 survived years of `signal()`.
159
+
160
+ The three adapters now re-export `collectFieldPaths`, `indexGroupPathMap` and
161
+ the `ValidationContext` type from core, so typing a validator's `context`
162
+ argument no longer means importing `@dynamic-field-kit/core` alongside the
163
+ adapter.
164
+
165
+ `@angular/platform-browser-dynamic`, which Angular 21 deprecates, is gone from
166
+ the package's devDependencies and from the demo app, which never used it - the
167
+ test setup now initialises through `@angular/platform-browser/testing`.
168
+
3
169
  ## 1.5.1
4
170
 
5
171
  ### Patch Changes
package/README.md CHANGED
@@ -37,7 +37,9 @@ npm install @dynamic-field-kit/core@^1.5.0 @dynamic-field-kit/angular@^1.5.0
37
37
  - `ColumnLayout` / `RowLayout` / `GridLayout` (the standalone layout components, registered for you)
38
38
  - `BaseInputComponent` — the abstract base your custom renderers extend
39
39
  - `FieldInputProps` — the inputs `BaseInputComponent` declares; the Angular
40
- mirror of core's `FieldRendererProps`
40
+ mirror of core's `FieldRendererProps`, and complete as of 1.6 — `touched`,
41
+ `dirty`, `id` and the aria flags used to be missing here, which left an
42
+ Angular renderer no way to tell whether a field had been touched
41
43
  - `DynamicFormOptions` — what `createDynamicFormStore` takes: `fields`,
42
44
  `initialValues`, `validateOnBlur`, `validateOnChange`
43
45
  - `LayoutConfig` / `ColumnLayoutConfig` / `RowLayoutConfig` /
@@ -51,20 +53,43 @@ both packages:
51
53
 
52
54
  - `validateField` / `validateFieldAsync` — one field, returns `string[]`
53
55
  - `validateFields` / `validateFieldsAsync` — a whole schema, returns `ValidationResult`
56
+ - `collectFieldPaths` — the leaf paths a schema actually has in the data (`contacts[0].email`)
57
+ - `indexGroupPathMap` — index an error or touched map by repeatable-group item
54
58
  - `resolveDisabled` / `resolveReadOnly` / `resolveOptions` — resolve a field's dynamic conditions and options
55
59
  - `validators` — the built-in validator helpers (`required`, `email`, `minLength`, `compose`, …)
56
60
  - `FieldDescription` / `FieldTypeKey` / `FieldRendererProps` — the schema and
57
61
  renderer contracts every adapter shares
58
- - `ValidationResult`
59
-
60
- `createDynamicFormStore` validates **synchronously** via `validateFields`,
61
- including on submit. Fields whose `validate` hook returns a Promise are treated
62
- as valid on that path, so run async rules through `validateFieldsAsync`
63
- yourself. See the
62
+ - `ValidationResult` / `ValidationContext`
63
+
64
+ `createDynamicFormStore` keeps live validation synchronous - a validator declared or
65
+ detected as async is never invoked on that path. Its `handleSubmit` runs one
66
+ async-capable pass, and `validateAsync()` is there when you need that answer
67
+ before submit. Runs are latest-wins: typing aborts the live run in flight, so a
68
+ stale result cannot overwrite a newer one, and a submit validates the snapshot
69
+ it was given under a controller of its own, so editing mid-submit no longer
70
+ cancels it. Declare a Promise-returning validator with
71
+ `validationMode: 'async'` and read `context.signal` (the fourth argument) to
72
+ cancel the request itself. See the
64
73
  [core README](https://github.com/vannt-dev/dynamic-field-kit/tree/develop/packages/core#sync-vs-async-validation)
65
74
  for the full rules.
66
75
 
67
- ## Basic setup (Angular 19+)
76
+ For a complete UI integration, see the
77
+ [Angular Material recipe](../../docs/ui-kit-recipes.md#angular--angular-material).
78
+
79
+ ## Supported Angular versions
80
+
81
+ The package declares `@angular/core` and `@angular/common` as `>=16 <22`,
82
+ and `scripts/verify-angular-peer-range.js` proves both ends in CI by installing
83
+ the packed tarballs against Angular 16 and 21 outside the workspace.
84
+
85
+ The floor is 16 because the form store is built on `signal` and `computed`,
86
+ which Angular introduced in 16. It read `>=14` until 1.6.0: npm accepted the
87
+ install on 14 and 15 and the package then failed on import.
88
+
89
+ The suite, the build and the demo app all run Angular 21, which is the version
90
+ the setup below is written for.
91
+
92
+ ## Basic setup
68
93
 
69
94
  1. Import the component and register fields before bootstrap.
70
95
 
@@ -141,6 +166,8 @@ import {
141
166
  <dfk-multi-field-input
142
167
  [fieldDescriptions]="fields"
143
168
  [properties]="store.data()"
169
+ [touched]="store.touched()"
170
+ [errors]="store.errors()"
144
171
  (onChange)="store.handleChange($event)"
145
172
  (onBlurField)="store.handleBlur($event)"
146
173
  ></dfk-multi-field-input>
@@ -165,14 +192,20 @@ export class MyForm {
165
192
  | ----------------------------------- | --------------------------------------------------------------------------------- |
166
193
  | `data()` | Current form data, with `computeValue` fields applied |
167
194
  | `errors()` | `Record<string, string[]>`, keyed like `validateFields` |
168
- | `isValid()` / `isDirty()` | No errors recorded / any value has changed |
195
+ | `isValid()` / `isDirty()` | Current synchronous validity / any value has changed |
196
+ | `isValidating()` | An async validation pass is in flight |
197
+ | `isValidationComplete()` | Every applicable validator finished and none is in flight |
198
+ | `validationStatus()` | `'valid' | 'invalid' | 'pending'`— prefer it over`isValid`alone:`valid` cannot tell "nothing is wrong" from "nothing is wrong yet" |
169
199
  | `isSubmitting()` / `isSubmitted()` | In-flight submit / at least one submit attempted |
170
200
  | `touched()` | Fields that have been blurred |
171
201
  | `handleChange(data)` | Replace the whole form data — bind to `(onChange)` |
172
202
  | `setFieldValue(name, value)` | Change one field |
173
203
  | `handleBlur(name)` | Mark touched, and validate when `validateOnBlur` |
174
204
  | `setFieldTouched(name, value?)` | Set touched explicitly |
205
+ | `touchAll()` | Mark every field touched — `handleSubmit` already calls it |
206
+ | `resetTouched()` | Clear touched only, leaving data/errors/dirty alone |
175
207
  | `validate()` | Validate now, returns a boolean |
208
+ | `validateAsync()` | Validate now, awaiting Promise-based rules |
176
209
  | `reset(values?)` | Back to `initialValues` (or the values given), clearing errors/touched/submission |
177
210
  | `handleSubmit(onValid, onInvalid?)` | Returns an async handler; calls `preventDefault`, validates, then dispatches |
178
211
 
@@ -180,6 +213,31 @@ export class MyForm {
180
213
  `focusout` listener — so it works with any renderer, without the renderer
181
214
  needing a blur output of its own.
182
215
 
216
+ Binding `[touched]="store.touched()"` and `[errors]="store.errors()"` makes the
217
+ store the single source of truth for renderer metadata. Touched state is what
218
+ makes an invalid submit visible: `handleSubmit` marks every
219
+ field touched before validating, so a renderer that gates its error on the
220
+ `touched` input shows it even for fields the user never focused. `reset()`
221
+ clears touched the same way. Leave `[touched]` unbound and `MultiFieldInput`
222
+ falls back to tracking it internally from blur alone; in that mode call its
223
+ public `resetTouched()` / `setFieldTouched(name, value?)` (via a `@ViewChild`)
224
+ to clear it, and listen to `(touchedChange)` for the next map.
225
+
226
+ ## Field ids
227
+
228
+ Each field renders with ``id={`${idPrefix}-${name}`}``, where `idPrefix`
229
+ defaults to a value unique to the `dfk-multi-field-input` instance. Two forms
230
+ containing a field of the same name therefore no longer emit the same DOM id
231
+ twice, and the id now reaches renderers as the `id` input.
232
+
233
+ ```html
234
+ <!-- pinned ids — reproduces the pre-1.6 `dfk-field-title` -->
235
+ <dfk-multi-field-input [fieldDescriptions]="fields" idPrefix="dfk-field">
236
+ </dfk-multi-field-input>
237
+ ```
238
+
239
+ For one field, set `id` on its `FieldDescription`; it wins over the prefix.
240
+
183
241
  ## Default renderers
184
242
 
185
243
  `text` · `number` · `password` · `email` · `textarea` · `checkbox` · `select` ·