@dynamic-field-kit/react 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 +166 -0
- package/README.md +76 -14
- package/dist/index.d.mts +79 -5
- package/dist/index.d.ts +79 -5
- package/dist/index.js +247 -100
- package/dist/index.mjs +262 -110
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,171 @@
|
|
|
1
1
|
# @dynamic-field-kit/react
|
|
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
|
@@ -41,16 +41,27 @@ both packages:
|
|
|
41
41
|
|
|
42
42
|
- `validateField` / `validateFieldAsync` — one field, returns `string[]`
|
|
43
43
|
- `validateFields` / `validateFieldsAsync` — a whole schema, returns `ValidationResult`
|
|
44
|
+
- `collectFieldPaths` — the leaf paths a schema actually has in the data (`contacts[0].email`)
|
|
45
|
+
- `indexGroupPathMap` — index an error or touched map by repeatable-group item
|
|
44
46
|
- `resolveDisabled` / `resolveReadOnly` / `resolveOptions` — resolve a field's dynamic conditions and options
|
|
45
47
|
- `validators` — the built-in validator helpers (`required`, `email`, `minLength`, `compose`, …)
|
|
46
|
-
- `ValidationResult`
|
|
47
|
-
|
|
48
|
-
`useDynamicForm`
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
- `ValidationResult` / `ValidationContext`
|
|
49
|
+
|
|
50
|
+
`useDynamicForm` keeps live validation synchronous - a validator declared or
|
|
51
|
+
detected as async is never invoked on that path. Its `handleSubmit` runs one
|
|
52
|
+
async-capable pass, and `validateAsync()` is there when you need that answer
|
|
53
|
+
before submit. Runs are latest-wins: typing aborts the live run in flight, so a
|
|
54
|
+
stale result cannot overwrite a newer one, and a submit validates the snapshot
|
|
55
|
+
it was given under a controller of its own, so editing mid-submit no longer
|
|
56
|
+
cancels it. Declare a Promise-returning validator with
|
|
57
|
+
`validationMode: 'async'` and read `context.signal` (the fourth argument) to
|
|
58
|
+
cancel the request itself. See the
|
|
51
59
|
[core README](https://github.com/vannt-dev/dynamic-field-kit/tree/develop/packages/core#sync-vs-async-validation)
|
|
52
60
|
for the full rules.
|
|
53
61
|
|
|
62
|
+
For a complete UI integration, see the
|
|
63
|
+
[Ant Design recipe](../../docs/ui-kit-recipes.md#react--ant-design).
|
|
64
|
+
|
|
54
65
|
`FieldGroupInput` (repeatable field groups) is used internally by `FieldInput` and doesn't need to be imported directly - see "Repeatable field groups" below.
|
|
55
66
|
|
|
56
67
|
Default layouts are registered automatically when you import the package root.
|
|
@@ -132,36 +143,87 @@ const form = useDynamicForm({
|
|
|
132
143
|
});
|
|
133
144
|
|
|
134
145
|
<form onSubmit={form.handleSubmit((data) => save(data))}>
|
|
135
|
-
<MultiFieldInput
|
|
136
|
-
fieldDescriptions={fields}
|
|
137
|
-
properties={form.data}
|
|
138
|
-
onChange={form.handleChange}
|
|
139
|
-
onBlurField={form.handleBlur} // wires touched + validateOnBlur
|
|
140
|
-
/>
|
|
146
|
+
<MultiFieldInput fieldDescriptions={fields} form={form} />
|
|
141
147
|
<button disabled={form.isSubmitting}>
|
|
142
148
|
{form.isSubmitting ? 'Saving…' : 'Save'}
|
|
143
149
|
</button>
|
|
144
150
|
</form>;
|
|
145
151
|
```
|
|
146
152
|
|
|
153
|
+
`form` is shorthand for five state/callback props, and is the recommended wiring:
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
<MultiFieldInput
|
|
157
|
+
fieldDescriptions={fields}
|
|
158
|
+
properties={form.data}
|
|
159
|
+
onChange={form.handleChange}
|
|
160
|
+
onBlurField={form.handleBlur} // touched + validateOnBlur
|
|
161
|
+
touched={form.touched} // makes the hook the only source of truth
|
|
162
|
+
errors={form.errors} // renderer and hook read the same error map
|
|
163
|
+
/>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Passing `touched` and `errors` gives the form store ownership of renderer
|
|
167
|
+
metadata. `touched` is what makes an invalid submit visible: `handleSubmit`
|
|
168
|
+
marks every field touched before validating, so a renderer that gates its error
|
|
169
|
+
on `touched` shows it even for fields the user never focused. `reset()` clears
|
|
170
|
+
touched the same way. Individually passed props win over the ones `form`
|
|
171
|
+
derives, so you can pass `form` and still override one wire.
|
|
172
|
+
|
|
147
173
|
| Member | Description |
|
|
148
174
|
| ----------------------------------- | --------------------------------------------------------------------------------- |
|
|
149
175
|
| `data` | Current form data, with `computeValue` fields applied |
|
|
150
176
|
| `errors` | `Record<string, string[]>`, keyed like `validateFields` |
|
|
151
|
-
| `isValid` / `isDirty` |
|
|
177
|
+
| `isValid` / `isDirty` | Current synchronous validity / any value has changed |
|
|
178
|
+
| `isValidating` | An async validation pass is in flight |
|
|
179
|
+
| `isValidationComplete` | Every applicable validator finished and none is in flight |
|
|
180
|
+
| `validationStatus` | `'valid' | 'invalid' | 'pending'`— prefer it over`isValid`alone:`valid` cannot tell "nothing is wrong" from "nothing is wrong yet" |
|
|
152
181
|
| `isSubmitting` / `isSubmitted` | In-flight submit / at least one submit attempted |
|
|
153
182
|
| `touched` | Fields that have been blurred |
|
|
154
183
|
| `handleChange(data)` | Replace the whole form data — pass to `MultiFieldInput`'s `onChange` |
|
|
155
184
|
| `setFieldValue(name, value)` | Change one field |
|
|
156
185
|
| `handleBlur(name)` | Mark touched, and validate when `validateOnBlur` |
|
|
157
186
|
| `setFieldTouched(name, value?)` | Set touched explicitly |
|
|
187
|
+
| `touchAll()` | Mark every field touched — `handleSubmit` already calls it |
|
|
188
|
+
| `resetTouched()` | Clear touched only, leaving data/errors/dirty alone |
|
|
189
|
+
| `setTouched` | Raw setter for the whole touched map |
|
|
158
190
|
| `setData` | Raw state setter, for escape hatches |
|
|
159
191
|
| `validate()` | Validate now, returns a boolean |
|
|
192
|
+
| `validateAsync()` | Validate now, awaiting Promise-based rules |
|
|
160
193
|
| `reset(values?)` | Back to `initialValues` (or the values given), clearing errors/touched/submission |
|
|
161
194
|
| `handleSubmit(onValid, onInvalid?)` | Returns a submit handler; calls `preventDefault`, validates, then dispatches |
|
|
162
195
|
|
|
163
|
-
`MultiFieldInput`
|
|
164
|
-
|
|
196
|
+
Leave `touched` off and `MultiFieldInput` falls back to tracking it internally
|
|
197
|
+
from blur alone, as it always did. In that mode nothing outside the component
|
|
198
|
+
can clear it — a form that stays mounted across submits will keep showing the
|
|
199
|
+
errors of the previous round after `reset()` — so it exposes a ref for it:
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
const ref = useRef<MultiFieldInputHandle>(null);
|
|
203
|
+
|
|
204
|
+
<MultiFieldInput ref={ref} fieldDescriptions={fields} />;
|
|
205
|
+
// after a successful submit
|
|
206
|
+
ref.current?.resetTouched();
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`resetTouched()`, `setFieldTouched(name, value?)` and `getTouched()` are the
|
|
210
|
+
handle's members. Controlled mode needs none of them: `form.reset()` covers it.
|
|
211
|
+
|
|
212
|
+
## Field ids
|
|
213
|
+
|
|
214
|
+
Each field renders with `id={`${idPrefix}-${name}`}`, where `idPrefix` defaults
|
|
215
|
+
to a value unique to the `MultiFieldInput` instance (from `useId`, so server and
|
|
216
|
+
client agree). Two forms containing a field of the same name therefore no longer
|
|
217
|
+
emit the same DOM id twice.
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
// pinned ids — reproduces the pre-1.6 `dfk-field-title`
|
|
221
|
+
<MultiFieldInput fieldDescriptions={fields} idPrefix="dfk-field" />
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
For one field, set `id` on its `FieldDescription`; it wins over the prefix.
|
|
225
|
+
Renderers receive the resolved value as the `id` prop, so a `<label htmlFor>` in
|
|
226
|
+
a renderer points at exactly one input.
|
|
165
227
|
|
|
166
228
|
## Default renderers
|
|
167
229
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { ReactNode, ComponentType } from 'react';
|
|
2
2
|
import { FieldTypeKey, Properties, FieldDescription, LayoutConfig, ValidationResult, FieldRendererProps, FieldTypeMap } from '@dynamic-field-kit/core';
|
|
3
|
-
export { FieldDescription, FieldRegistry, FieldRendererProps, FieldTypeKey, LayoutConfig, ValidationResult, resolveDisabled, resolveOptions, resolveReadOnly, validateField, validateFieldAsync, validateFields, validateFieldsAsync, validators } from '@dynamic-field-kit/core';
|
|
3
|
+
export { FIELD_RENDERER_PROP_KEYS, FieldDescription, FieldRegistry, FieldRendererProps, FieldTypeKey, LayoutConfig, ValidationContext, ValidationResult, buildFieldRendererProps, collectFieldPaths, indexGroupPathMap, makeFieldId, resolveDisabled, resolveOptions, resolveReadOnly, validateField, validateFieldAsync, validateFields, validateFieldsAsync, validators } from '@dynamic-field-kit/core';
|
|
4
4
|
|
|
5
5
|
type LayoutRenderer<C = unknown> = (props: {
|
|
6
6
|
children: React.ReactNode;
|
|
@@ -19,6 +19,7 @@ interface Props$2<T extends FieldTypeKey> {
|
|
|
19
19
|
onChange?: (value: unknown) => void;
|
|
20
20
|
onBlur?: () => void;
|
|
21
21
|
label?: string;
|
|
22
|
+
placeholder?: string;
|
|
22
23
|
options?: Properties[];
|
|
23
24
|
className?: string;
|
|
24
25
|
description?: ReactNode;
|
|
@@ -32,28 +33,68 @@ interface Props$2<T extends FieldTypeKey> {
|
|
|
32
33
|
ariaInvalid?: boolean;
|
|
33
34
|
ariaDescribedBy?: string;
|
|
34
35
|
ariaRequired?: boolean;
|
|
36
|
+
min?: number | string;
|
|
37
|
+
max?: number | string;
|
|
38
|
+
step?: number | string;
|
|
39
|
+
accept?: string;
|
|
40
|
+
multiple?: boolean;
|
|
35
41
|
/** Extra, framework-agnostic props forwarded verbatim to the renderer. */
|
|
36
42
|
extraProps?: Properties;
|
|
37
43
|
}
|
|
38
|
-
declare const DynamicInputInner: <T extends FieldTypeKey>({ type,
|
|
44
|
+
declare const DynamicInputInner: <T extends FieldTypeKey>({ type, onChange, onBlur, extraProps, ...rendererProps }: Props$2<T>) => React.JSX.Element;
|
|
39
45
|
declare const DynamicInput: typeof DynamicInputInner;
|
|
40
46
|
|
|
41
47
|
interface Props$1 {
|
|
42
48
|
fieldDescription: FieldDescription;
|
|
43
49
|
renderInfos: Properties;
|
|
44
50
|
rootData?: Properties;
|
|
51
|
+
/** Per-form-instance id namespace; see core's `makeFieldId`. */
|
|
52
|
+
idPrefix?: string;
|
|
45
53
|
touched?: boolean;
|
|
54
|
+
touchedMap?: Record<string, boolean>;
|
|
46
55
|
dirty?: boolean;
|
|
56
|
+
errors?: Record<string, string[]>;
|
|
47
57
|
onBlurField?: (key: string) => void;
|
|
48
58
|
onValueChangeField: (value: unknown, key: string) => void;
|
|
49
59
|
}
|
|
50
|
-
declare const FieldInput: React.MemoExoticComponent<({ fieldDescription, renderInfos, rootData, touched, dirty, onBlurField, onValueChangeField, }: Props$1) => React.JSX.Element>;
|
|
60
|
+
declare const FieldInput: React.MemoExoticComponent<({ fieldDescription, renderInfos, rootData, idPrefix, touched, touchedMap, dirty, errors, onBlurField, onValueChangeField, }: Props$1) => React.JSX.Element>;
|
|
51
61
|
|
|
62
|
+
/**
|
|
63
|
+
* The slice of `useDynamicForm`'s result `MultiFieldInput` needs to drive
|
|
64
|
+
* itself. Structural, so the hook result can be passed straight in.
|
|
65
|
+
*/
|
|
66
|
+
interface DynamicFormBinding {
|
|
67
|
+
data: Properties;
|
|
68
|
+
errors: Record<string, string[]>;
|
|
69
|
+
touched: Record<string, boolean>;
|
|
70
|
+
handleChange: (data: Properties) => void;
|
|
71
|
+
handleBlur: (fieldName: string) => void;
|
|
72
|
+
}
|
|
73
|
+
/** Imperative handle exposed on a `MultiFieldInput` ref. */
|
|
74
|
+
interface MultiFieldInputHandle {
|
|
75
|
+
/**
|
|
76
|
+
* Clears the internally tracked touched state. Only meaningful in
|
|
77
|
+
* uncontrolled mode - when `touched` is passed as a prop, resetting the form
|
|
78
|
+
* store (e.g. `useDynamicForm().reset()`) already clears it.
|
|
79
|
+
*/
|
|
80
|
+
resetTouched: () => void;
|
|
81
|
+
setFieldTouched: (fieldName: string, isTouched?: boolean) => void;
|
|
82
|
+
/** The touched map currently in effect, controlled or internal. */
|
|
83
|
+
getTouched: () => Record<string, boolean>;
|
|
84
|
+
}
|
|
52
85
|
interface Props {
|
|
53
86
|
fieldDescriptions: FieldDescription[];
|
|
54
87
|
properties?: Properties;
|
|
55
88
|
onChange?: (data: Properties) => void;
|
|
56
89
|
layout?: LayoutConfig;
|
|
90
|
+
/**
|
|
91
|
+
* Namespace for generated field ids: a field renders with
|
|
92
|
+
* `${idPrefix}-${name}`. Defaults to a value unique to this component
|
|
93
|
+
* instance, so two forms containing the same field name do not emit
|
|
94
|
+
* duplicate DOM ids. Pass a fixed string to pin ids (`idPrefix="dfk-field"`
|
|
95
|
+
* restores the pre-1.6 ids), or set `FieldDescription.id` per field.
|
|
96
|
+
*/
|
|
97
|
+
idPrefix?: string;
|
|
57
98
|
/**
|
|
58
99
|
* Top-level form data, threaded down through repeatable groups so a nested
|
|
59
100
|
* field's `appearCondition`/`computeValue` can read the root form. Omitted at
|
|
@@ -73,8 +114,26 @@ interface Props {
|
|
|
73
114
|
* map and `validateOnBlur` behaviour.
|
|
74
115
|
*/
|
|
75
116
|
onBlurField?: (fieldName: string) => void;
|
|
117
|
+
/**
|
|
118
|
+
* Controlled touched map. When provided it is the single source of truth and
|
|
119
|
+
* the internal tracker is bypassed entirely, so `useDynamicForm().touched`
|
|
120
|
+
* (updated by `setFieldTouched`, `touchAll`, `handleSubmit` and cleared by
|
|
121
|
+
* `reset`) is what renderers actually see. Omit it to keep the internal,
|
|
122
|
+
* blur-only tracker.
|
|
123
|
+
*/
|
|
124
|
+
touched?: Record<string, boolean>;
|
|
125
|
+
/** Controlled validation errors. Empty means no renderer error. */
|
|
126
|
+
errors?: Record<string, string[]>;
|
|
127
|
+
/** Fires with the next touched map whenever a field is blurred. */
|
|
128
|
+
onTouchedChange?: (touched: Record<string, boolean>) => void;
|
|
129
|
+
/**
|
|
130
|
+
* Shorthand that wires `properties`, `onChange`, `onBlurField` and `touched`
|
|
131
|
+
* from a `useDynamicForm` result in one prop. Individually passed props win
|
|
132
|
+
* over the ones derived from here.
|
|
133
|
+
*/
|
|
134
|
+
form?: DynamicFormBinding;
|
|
76
135
|
}
|
|
77
|
-
declare const MultiFieldInput:
|
|
136
|
+
declare const MultiFieldInput: React.ForwardRefExoticComponent<Props & React.RefAttributes<MultiFieldInputHandle>>;
|
|
78
137
|
|
|
79
138
|
interface DynamicFormDevToolsProps {
|
|
80
139
|
data: Properties;
|
|
@@ -96,6 +155,9 @@ interface UseDynamicFormResult {
|
|
|
96
155
|
data: Properties;
|
|
97
156
|
errors: Record<string, string[]>;
|
|
98
157
|
isValid: boolean;
|
|
158
|
+
isValidating: boolean;
|
|
159
|
+
isValidationComplete: boolean;
|
|
160
|
+
validationStatus: ValidationResult['status'];
|
|
99
161
|
isDirty: boolean;
|
|
100
162
|
isSubmitting: boolean;
|
|
101
163
|
isSubmitted: boolean;
|
|
@@ -103,10 +165,22 @@ interface UseDynamicFormResult {
|
|
|
103
165
|
setData: React.Dispatch<React.SetStateAction<Properties>>;
|
|
104
166
|
setFieldValue: (name: string, value: unknown) => void;
|
|
105
167
|
setFieldTouched: (name: string, isTouched?: boolean) => void;
|
|
168
|
+
/** Replaces the whole touched map. */
|
|
169
|
+
setTouched: React.Dispatch<React.SetStateAction<Record<string, boolean>>>;
|
|
170
|
+
/**
|
|
171
|
+
* Marks every field touched at once. `handleSubmit` calls this for you, so
|
|
172
|
+
* an invalid submit surfaces errors on fields the user never focused - pass
|
|
173
|
+
* `touched` into `MultiFieldInput` for it to take effect.
|
|
174
|
+
*/
|
|
175
|
+
touchAll: () => void;
|
|
176
|
+
/** Clears the touched map without touching data, errors or dirty state. */
|
|
177
|
+
resetTouched: () => void;
|
|
106
178
|
handleChange: (newData: Properties) => void;
|
|
107
179
|
handleBlur: (fieldName: string) => void;
|
|
108
180
|
reset: (newValues?: Properties) => void;
|
|
109
181
|
validate: () => boolean;
|
|
182
|
+
/** Validate all fields and await Promise-based rules. */
|
|
183
|
+
validateAsync: () => Promise<boolean>;
|
|
110
184
|
handleSubmit: (onValid: (data: Properties) => void | Promise<void>, onInvalid?: (errors: Record<string, string[]>) => void) => (e?: React.FormEvent) => Promise<void>;
|
|
111
185
|
}
|
|
112
186
|
declare function useDynamicForm({ fields, initialValues, validateOnBlur, validateOnChange, }: UseDynamicFormOptions): UseDynamicFormResult;
|
|
@@ -129,4 +203,4 @@ declare const FieldRegistryProvider: ({ registry, children, }: FieldRegistryProv
|
|
|
129
203
|
/** The registry for the nearest provider, or the global singleton. */
|
|
130
204
|
declare function useFieldRegistry(): ReactFieldRegistry;
|
|
131
205
|
|
|
132
|
-
export { DynamicFormDevTools, DynamicInput, FieldInput, FieldRegistryProvider, type FieldRegistryProviderProps, MultiFieldInput, type ReactFieldRegistry, type ReactFieldRenderer, defaultRenderersMap, fieldRegistry, getDefaultRenderer, layoutRegistry, useDynamicForm, useFieldRegistry };
|
|
206
|
+
export { type DynamicFormBinding, DynamicFormDevTools, DynamicInput, FieldInput, FieldRegistryProvider, type FieldRegistryProviderProps, MultiFieldInput, type MultiFieldInputHandle, type ReactFieldRegistry, type ReactFieldRenderer, type UseDynamicFormOptions, type UseDynamicFormResult, defaultRenderersMap, fieldRegistry, getDefaultRenderer, layoutRegistry, useDynamicForm, useFieldRegistry };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { ReactNode, ComponentType } from 'react';
|
|
2
2
|
import { FieldTypeKey, Properties, FieldDescription, LayoutConfig, ValidationResult, FieldRendererProps, FieldTypeMap } from '@dynamic-field-kit/core';
|
|
3
|
-
export { FieldDescription, FieldRegistry, FieldRendererProps, FieldTypeKey, LayoutConfig, ValidationResult, resolveDisabled, resolveOptions, resolveReadOnly, validateField, validateFieldAsync, validateFields, validateFieldsAsync, validators } from '@dynamic-field-kit/core';
|
|
3
|
+
export { FIELD_RENDERER_PROP_KEYS, FieldDescription, FieldRegistry, FieldRendererProps, FieldTypeKey, LayoutConfig, ValidationContext, ValidationResult, buildFieldRendererProps, collectFieldPaths, indexGroupPathMap, makeFieldId, resolveDisabled, resolveOptions, resolveReadOnly, validateField, validateFieldAsync, validateFields, validateFieldsAsync, validators } from '@dynamic-field-kit/core';
|
|
4
4
|
|
|
5
5
|
type LayoutRenderer<C = unknown> = (props: {
|
|
6
6
|
children: React.ReactNode;
|
|
@@ -19,6 +19,7 @@ interface Props$2<T extends FieldTypeKey> {
|
|
|
19
19
|
onChange?: (value: unknown) => void;
|
|
20
20
|
onBlur?: () => void;
|
|
21
21
|
label?: string;
|
|
22
|
+
placeholder?: string;
|
|
22
23
|
options?: Properties[];
|
|
23
24
|
className?: string;
|
|
24
25
|
description?: ReactNode;
|
|
@@ -32,28 +33,68 @@ interface Props$2<T extends FieldTypeKey> {
|
|
|
32
33
|
ariaInvalid?: boolean;
|
|
33
34
|
ariaDescribedBy?: string;
|
|
34
35
|
ariaRequired?: boolean;
|
|
36
|
+
min?: number | string;
|
|
37
|
+
max?: number | string;
|
|
38
|
+
step?: number | string;
|
|
39
|
+
accept?: string;
|
|
40
|
+
multiple?: boolean;
|
|
35
41
|
/** Extra, framework-agnostic props forwarded verbatim to the renderer. */
|
|
36
42
|
extraProps?: Properties;
|
|
37
43
|
}
|
|
38
|
-
declare const DynamicInputInner: <T extends FieldTypeKey>({ type,
|
|
44
|
+
declare const DynamicInputInner: <T extends FieldTypeKey>({ type, onChange, onBlur, extraProps, ...rendererProps }: Props$2<T>) => React.JSX.Element;
|
|
39
45
|
declare const DynamicInput: typeof DynamicInputInner;
|
|
40
46
|
|
|
41
47
|
interface Props$1 {
|
|
42
48
|
fieldDescription: FieldDescription;
|
|
43
49
|
renderInfos: Properties;
|
|
44
50
|
rootData?: Properties;
|
|
51
|
+
/** Per-form-instance id namespace; see core's `makeFieldId`. */
|
|
52
|
+
idPrefix?: string;
|
|
45
53
|
touched?: boolean;
|
|
54
|
+
touchedMap?: Record<string, boolean>;
|
|
46
55
|
dirty?: boolean;
|
|
56
|
+
errors?: Record<string, string[]>;
|
|
47
57
|
onBlurField?: (key: string) => void;
|
|
48
58
|
onValueChangeField: (value: unknown, key: string) => void;
|
|
49
59
|
}
|
|
50
|
-
declare const FieldInput: React.MemoExoticComponent<({ fieldDescription, renderInfos, rootData, touched, dirty, onBlurField, onValueChangeField, }: Props$1) => React.JSX.Element>;
|
|
60
|
+
declare const FieldInput: React.MemoExoticComponent<({ fieldDescription, renderInfos, rootData, idPrefix, touched, touchedMap, dirty, errors, onBlurField, onValueChangeField, }: Props$1) => React.JSX.Element>;
|
|
51
61
|
|
|
62
|
+
/**
|
|
63
|
+
* The slice of `useDynamicForm`'s result `MultiFieldInput` needs to drive
|
|
64
|
+
* itself. Structural, so the hook result can be passed straight in.
|
|
65
|
+
*/
|
|
66
|
+
interface DynamicFormBinding {
|
|
67
|
+
data: Properties;
|
|
68
|
+
errors: Record<string, string[]>;
|
|
69
|
+
touched: Record<string, boolean>;
|
|
70
|
+
handleChange: (data: Properties) => void;
|
|
71
|
+
handleBlur: (fieldName: string) => void;
|
|
72
|
+
}
|
|
73
|
+
/** Imperative handle exposed on a `MultiFieldInput` ref. */
|
|
74
|
+
interface MultiFieldInputHandle {
|
|
75
|
+
/**
|
|
76
|
+
* Clears the internally tracked touched state. Only meaningful in
|
|
77
|
+
* uncontrolled mode - when `touched` is passed as a prop, resetting the form
|
|
78
|
+
* store (e.g. `useDynamicForm().reset()`) already clears it.
|
|
79
|
+
*/
|
|
80
|
+
resetTouched: () => void;
|
|
81
|
+
setFieldTouched: (fieldName: string, isTouched?: boolean) => void;
|
|
82
|
+
/** The touched map currently in effect, controlled or internal. */
|
|
83
|
+
getTouched: () => Record<string, boolean>;
|
|
84
|
+
}
|
|
52
85
|
interface Props {
|
|
53
86
|
fieldDescriptions: FieldDescription[];
|
|
54
87
|
properties?: Properties;
|
|
55
88
|
onChange?: (data: Properties) => void;
|
|
56
89
|
layout?: LayoutConfig;
|
|
90
|
+
/**
|
|
91
|
+
* Namespace for generated field ids: a field renders with
|
|
92
|
+
* `${idPrefix}-${name}`. Defaults to a value unique to this component
|
|
93
|
+
* instance, so two forms containing the same field name do not emit
|
|
94
|
+
* duplicate DOM ids. Pass a fixed string to pin ids (`idPrefix="dfk-field"`
|
|
95
|
+
* restores the pre-1.6 ids), or set `FieldDescription.id` per field.
|
|
96
|
+
*/
|
|
97
|
+
idPrefix?: string;
|
|
57
98
|
/**
|
|
58
99
|
* Top-level form data, threaded down through repeatable groups so a nested
|
|
59
100
|
* field's `appearCondition`/`computeValue` can read the root form. Omitted at
|
|
@@ -73,8 +114,26 @@ interface Props {
|
|
|
73
114
|
* map and `validateOnBlur` behaviour.
|
|
74
115
|
*/
|
|
75
116
|
onBlurField?: (fieldName: string) => void;
|
|
117
|
+
/**
|
|
118
|
+
* Controlled touched map. When provided it is the single source of truth and
|
|
119
|
+
* the internal tracker is bypassed entirely, so `useDynamicForm().touched`
|
|
120
|
+
* (updated by `setFieldTouched`, `touchAll`, `handleSubmit` and cleared by
|
|
121
|
+
* `reset`) is what renderers actually see. Omit it to keep the internal,
|
|
122
|
+
* blur-only tracker.
|
|
123
|
+
*/
|
|
124
|
+
touched?: Record<string, boolean>;
|
|
125
|
+
/** Controlled validation errors. Empty means no renderer error. */
|
|
126
|
+
errors?: Record<string, string[]>;
|
|
127
|
+
/** Fires with the next touched map whenever a field is blurred. */
|
|
128
|
+
onTouchedChange?: (touched: Record<string, boolean>) => void;
|
|
129
|
+
/**
|
|
130
|
+
* Shorthand that wires `properties`, `onChange`, `onBlurField` and `touched`
|
|
131
|
+
* from a `useDynamicForm` result in one prop. Individually passed props win
|
|
132
|
+
* over the ones derived from here.
|
|
133
|
+
*/
|
|
134
|
+
form?: DynamicFormBinding;
|
|
76
135
|
}
|
|
77
|
-
declare const MultiFieldInput:
|
|
136
|
+
declare const MultiFieldInput: React.ForwardRefExoticComponent<Props & React.RefAttributes<MultiFieldInputHandle>>;
|
|
78
137
|
|
|
79
138
|
interface DynamicFormDevToolsProps {
|
|
80
139
|
data: Properties;
|
|
@@ -96,6 +155,9 @@ interface UseDynamicFormResult {
|
|
|
96
155
|
data: Properties;
|
|
97
156
|
errors: Record<string, string[]>;
|
|
98
157
|
isValid: boolean;
|
|
158
|
+
isValidating: boolean;
|
|
159
|
+
isValidationComplete: boolean;
|
|
160
|
+
validationStatus: ValidationResult['status'];
|
|
99
161
|
isDirty: boolean;
|
|
100
162
|
isSubmitting: boolean;
|
|
101
163
|
isSubmitted: boolean;
|
|
@@ -103,10 +165,22 @@ interface UseDynamicFormResult {
|
|
|
103
165
|
setData: React.Dispatch<React.SetStateAction<Properties>>;
|
|
104
166
|
setFieldValue: (name: string, value: unknown) => void;
|
|
105
167
|
setFieldTouched: (name: string, isTouched?: boolean) => void;
|
|
168
|
+
/** Replaces the whole touched map. */
|
|
169
|
+
setTouched: React.Dispatch<React.SetStateAction<Record<string, boolean>>>;
|
|
170
|
+
/**
|
|
171
|
+
* Marks every field touched at once. `handleSubmit` calls this for you, so
|
|
172
|
+
* an invalid submit surfaces errors on fields the user never focused - pass
|
|
173
|
+
* `touched` into `MultiFieldInput` for it to take effect.
|
|
174
|
+
*/
|
|
175
|
+
touchAll: () => void;
|
|
176
|
+
/** Clears the touched map without touching data, errors or dirty state. */
|
|
177
|
+
resetTouched: () => void;
|
|
106
178
|
handleChange: (newData: Properties) => void;
|
|
107
179
|
handleBlur: (fieldName: string) => void;
|
|
108
180
|
reset: (newValues?: Properties) => void;
|
|
109
181
|
validate: () => boolean;
|
|
182
|
+
/** Validate all fields and await Promise-based rules. */
|
|
183
|
+
validateAsync: () => Promise<boolean>;
|
|
110
184
|
handleSubmit: (onValid: (data: Properties) => void | Promise<void>, onInvalid?: (errors: Record<string, string[]>) => void) => (e?: React.FormEvent) => Promise<void>;
|
|
111
185
|
}
|
|
112
186
|
declare function useDynamicForm({ fields, initialValues, validateOnBlur, validateOnChange, }: UseDynamicFormOptions): UseDynamicFormResult;
|
|
@@ -129,4 +203,4 @@ declare const FieldRegistryProvider: ({ registry, children, }: FieldRegistryProv
|
|
|
129
203
|
/** The registry for the nearest provider, or the global singleton. */
|
|
130
204
|
declare function useFieldRegistry(): ReactFieldRegistry;
|
|
131
205
|
|
|
132
|
-
export { DynamicFormDevTools, DynamicInput, FieldInput, FieldRegistryProvider, type FieldRegistryProviderProps, MultiFieldInput, type ReactFieldRegistry, type ReactFieldRenderer, defaultRenderersMap, fieldRegistry, getDefaultRenderer, layoutRegistry, useDynamicForm, useFieldRegistry };
|
|
206
|
+
export { type DynamicFormBinding, DynamicFormDevTools, DynamicInput, FieldInput, FieldRegistryProvider, type FieldRegistryProviderProps, MultiFieldInput, type MultiFieldInputHandle, type ReactFieldRegistry, type ReactFieldRenderer, type UseDynamicFormOptions, type UseDynamicFormResult, defaultRenderersMap, fieldRegistry, getDefaultRenderer, layoutRegistry, useDynamicForm, useFieldRegistry };
|