@dynamic-field-kit/react 1.3.0 → 1.5.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 +180 -0
- package/LICENSE +21 -0
- package/README.md +161 -4
- package/dist/index.d.mts +88 -18
- package/dist/index.d.ts +88 -18
- package/dist/index.js +843 -62
- package/dist/index.mjs +832 -48
- package/package.json +38 -24
- package/dist/index.js.map +0 -1
- package/dist/index.mjs.map +0 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# @dynamic-field-kit/react
|
|
2
|
+
|
|
3
|
+
## 1.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Form state hooks, schema adapters, wizard engine, DevTools and extended renderers across all four packages, with type declarations that now resolve correctly under node16 and nodenext.
|
|
8
|
+
- d91c737: Add form state hooks, schema adapters, wizard engine, DevTools and extended renderers.
|
|
9
|
+
|
|
10
|
+
**Core**
|
|
11
|
+
|
|
12
|
+
- `zodValidator`, `yupValidator`, `valibotValidator` / `standardSchemaValidator`. Adapters parse **synchronously** so their result is usable by the synchronous `validateFields`; schemas with async refinements or async `.test()` rules return a Promise and must be validated through `validateFieldsAsync`.
|
|
13
|
+
- Adapters take an explicit `{ target: 'form' | 'field' }` option. `'form'` (the default) parses the form data object; `'field'` parses a single scalar value. The field-name shorthand — `zodValidator(schema, 'email')` — is unchanged.
|
|
14
|
+
- Wizard engine: `createWizardState`, `validateStep`, `canGoNext`, `canGoPrev`, plus the navigation the engine needs to be usable - `goNext`, `goPrev`, `goToStep`, `markStepCompleted`, `isStepCompleted`. `goNext` records the step it leaves, so `completedSteps` is actually maintained.
|
|
15
|
+
- Group array helpers: `moveGroupItem`, `swapGroupItems`, `insertGroupItem`, `focusFirstInvalidField`.
|
|
16
|
+
- `switch` is a first-class field type: it had a shipped renderer in react and vue but no `FieldTypeMap` entry, so `type: 'switch'` did not typecheck.
|
|
17
|
+
|
|
18
|
+
**React / Vue / Angular**
|
|
19
|
+
|
|
20
|
+
- `useDynamicForm` (React, Vue) and `createDynamicFormStore` (Angular Signals) now expose the same surface, including `isSubmitting` and `isSubmitted`.
|
|
21
|
+
- `handleSubmit(onValid, onInvalid)` returns a submit handler in every framework and calls `preventDefault` on the event it receives.
|
|
22
|
+
- Default HTML5 renderers for `radio`, `range`, `file`, `date`, `time`, `datetime-local` and `switch`.
|
|
23
|
+
- `DynamicFormDevTools` overlay for inspecting form data, errors, metadata and field descriptions, with an error-count badge in all three frameworks.
|
|
24
|
+
- `MultiFieldInput` reports blur through `onBlurField` (an `@Output` in Angular), so a form store's `handleBlur` / `touched` / `validateOnBlur` can be wired to it. Vue and Angular previously had no blur plumbing at all.
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
- c3faa51: Document the v1.5 APIs in each package README: form state, schema adapters, the wizard engine, default renderers, DevTools and blur wiring. README ships in the npm tarball, so this reaches package pages only through a release.
|
|
29
|
+
- 75447a6: Close the remaining documentation gaps in each package README, so every public
|
|
30
|
+
export is described somewhere. README ships in the npm tarball, so this reaches
|
|
31
|
+
the package pages only through a release.
|
|
32
|
+
|
|
33
|
+
- **Sync vs async validation** is now spelled out in core, with the consequence
|
|
34
|
+
that was previously implicit: `validateField` / `validateFields` cannot await,
|
|
35
|
+
so a `validate` hook returning a Promise is treated as valid on the sync path.
|
|
36
|
+
`useDynamicForm` and `createDynamicFormStore` validate synchronously
|
|
37
|
+
(including on submit), so async rules have to run through
|
|
38
|
+
`validateFieldsAsync` explicitly. Each adapter README repeats the caveat and
|
|
39
|
+
links to the core section.
|
|
40
|
+
- Document `validateFieldAsync`, `validateFieldsAsync`, `resolveOptions` and
|
|
41
|
+
`validators` in the react, vue and angular export lists, separated from each
|
|
42
|
+
adapter's own exports so it is clear they are core re-exports.
|
|
43
|
+
- Document the core types that appear in every signature but had no definition
|
|
44
|
+
in the README: `Properties`, `ValidatorFn`, `FieldValidatorResult`,
|
|
45
|
+
`FieldValidatorFunction`, and `FormStep` alongside `WizardState`.
|
|
46
|
+
- Angular: document `layoutRegistry` / `LayoutRegistry`, the `ColumnLayout` /
|
|
47
|
+
`RowLayout` / `GridLayout` components and `BaseInputComponent`, with a custom
|
|
48
|
+
layout example. Its layout registry holds standalone components rather than
|
|
49
|
+
render functions, which is the one place the three adapters genuinely differ,
|
|
50
|
+
and it was the only adapter not documenting its registry at all.
|
|
51
|
+
- Rename angular's `## What it exports` to `## Exports` to match react and vue.
|
|
52
|
+
- Link the demo sub-routes: react's enterprise-features and wizard pages, and
|
|
53
|
+
from core the per-framework demos plus the wizard it documents.
|
|
54
|
+
|
|
55
|
+
- e10044b: Fix the type declarations consumers get under `moduleResolution: node16` /
|
|
56
|
+
`nodenext`, and fill in the npm metadata the package pages never had.
|
|
57
|
+
|
|
58
|
+
Every package declared a single `types` target for both module formats while
|
|
59
|
+
shipping two sets of declarations. `arethetypeswrong` on the built tarballs
|
|
60
|
+
reported core and react "masquerading as CJS" and vue "masquerading as ESM":
|
|
61
|
+
an ESM import resolved to the CommonJS declaration file, and vice versa. The
|
|
62
|
+
`exports` maps now declare `types` per condition, so each format resolves to
|
|
63
|
+
the declarations that describe it.
|
|
64
|
+
|
|
65
|
+
Angular was worse than a mismatch - it did not resolve at all. Its entry is an
|
|
66
|
+
`.mjs` bundle, so TypeScript reads its declarations in ESM mode, where the
|
|
67
|
+
extensionless `export * from './public-api'` that ng-packagr generates is error
|
|
68
|
+
TS2834. The failure is in `dist/index.d.ts`, the first file a consumer reaches,
|
|
69
|
+
so the package was unusable on node16 resolution. A `postbuild` step now adds
|
|
70
|
+
the explicit `.js` extension to relative specifiers in the emitted `.d.ts`
|
|
71
|
+
files, resolving each against the build output so a directory import becomes
|
|
72
|
+
`./layout/index.js` rather than a broken `./layout.js`. Angular also gains an
|
|
73
|
+
`exports` map and `"type": "module"`, which it needs to describe itself
|
|
74
|
+
honestly. All four packages are clean on all four resolution modes now, except
|
|
75
|
+
angular's `require()`, which is ESM-only by nature - as `@angular/core` is.
|
|
76
|
+
|
|
77
|
+
Also in the tarballs: `CHANGELOG.md` now ships, so the npm page has release
|
|
78
|
+
history, and angular no longer ships a second copy of its README and LICENSE
|
|
79
|
+
that ng-packagr had copied into `dist`. Every package gains `homepage`, `bugs`
|
|
80
|
+
and `repository.directory`, so npm links to the right README and issue tracker
|
|
81
|
+
instead of nothing, and `publishConfig.provenance`, so each published tarball
|
|
82
|
+
is signed and linked back to the workflow run that built it.
|
|
83
|
+
|
|
84
|
+
- 75447a6: Let consumer bundlers drop the parts of the adapters an app does not use.
|
|
85
|
+
|
|
86
|
+
Components were declared as bare top-level calls — `defineComponent({...})` in
|
|
87
|
+
vue, `React.memo(...)` in react. A bundler cannot prove such a call is
|
|
88
|
+
side-effect free, so it has to evaluate it even when the result is unused, which
|
|
89
|
+
kept every default renderer and every component in an app's bundle no matter how
|
|
90
|
+
little of the package it imported. Marking those calls `/* @__PURE__ */` makes
|
|
91
|
+
them droppable. Measured with esbuild, minified, framework external:
|
|
92
|
+
|
|
93
|
+
| App imports | Before | After |
|
|
94
|
+
| -------------------------- | -------- | --------------- |
|
|
95
|
+
| react: one core helper | 11,149 B | 3,258 B (−71%) |
|
|
96
|
+
| react: `DynamicInput` only | 11,150 B | 6,830 B (−39%) |
|
|
97
|
+
| react: everything | 17,601 B | unchanged |
|
|
98
|
+
| vue: one core helper | 17,379 B | 13,934 B (−20%) |
|
|
99
|
+
| vue: `DynamicInput` only | 17,379 B | 13,934 B (−20%) |
|
|
100
|
+
| vue: everything | 20,530 B | unchanged |
|
|
101
|
+
|
|
102
|
+
Apps that use the whole surface are unchanged, which is the expected result —
|
|
103
|
+
there is nothing to drop. The shipped `dist` grows slightly (react +0.03 KB, vue
|
|
104
|
+
+0.29 KB) because the annotations are comments in the bundle; the trade is a
|
|
105
|
+
bigger published file for a smaller consumer bundle.
|
|
106
|
+
|
|
107
|
+
`@dynamic-field-kit/core` now declares `"sideEffects": false`. It has no
|
|
108
|
+
top-level execution at all — the only module-scope work is `new FieldRegistry()`
|
|
109
|
+
assigned to an export — so the claim is accurate, and it lets bundlers that rely
|
|
110
|
+
on the flag rather than their own analysis skip core entirely when it is unused.
|
|
111
|
+
The adapters deliberately do not set it: their entry side-effect-imports the
|
|
112
|
+
default layouts in order to register them.
|
|
113
|
+
|
|
114
|
+
- 75447a6: Stop publishing sourcemaps, roughly halving what each package installs.
|
|
115
|
+
|
|
116
|
+
tsup was emitting sourcemaps with `sourcesContent`, so every `.map` carried a
|
|
117
|
+
full copy of the TypeScript source. That is what made them work at all — `files`
|
|
118
|
+
only publishes `dist`, so a map referencing `../src/*.ts` would otherwise
|
|
119
|
+
resolve to nothing — but it also made them about half of each tarball, shipped
|
|
120
|
+
to every consumer on every install.
|
|
121
|
+
|
|
122
|
+
| Package | Unpacked | Tarball | Files |
|
|
123
|
+
| ------- | ----------------------- | --------------------- | ----- |
|
|
124
|
+
| core | 157.0 → 83.0 KB (−47%) | 33.3 → 17.5 KB (−47%) | 8 → 6 |
|
|
125
|
+
| react | 207.6 → 90.1 KB (−57%) | 46.8 → 19.2 KB (−59%) | 8 → 6 |
|
|
126
|
+
| vue | 250.4 → 109.4 KB (−56%) | 47.6 → 20.5 KB (−57%) | 8 → 6 |
|
|
127
|
+
|
|
128
|
+
Nothing that ends up in an application bundle changes — sourcemaps never do.
|
|
129
|
+
What changes is install size, and the ability to step into the library's
|
|
130
|
+
TypeScript source while debugging a consuming app.
|
|
131
|
+
|
|
132
|
+
This is a deliberate trade, not a free win: the maps worked. Each package's
|
|
133
|
+
`tsup.config.ts` carries the reasoning next to a `sourcemap: false` that is one
|
|
134
|
+
word away from restoring them.
|
|
135
|
+
|
|
136
|
+
`@dynamic-field-kit/angular` is unaffected; ng-packagr's published output does
|
|
137
|
+
not carry them.
|
|
138
|
+
|
|
139
|
+
- 244c3d4: Ship the MIT license text, and fix what the angular package tells npm.
|
|
140
|
+
|
|
141
|
+
Every package declared `"license": "MIT"` with no LICENSE file anywhere in the
|
|
142
|
+
repo, so the tarballs carried the claim and not the terms. npm includes a
|
|
143
|
+
LICENSE at the package root regardless of `files`, so all four now ship one.
|
|
144
|
+
|
|
145
|
+
The angular package's README was wrong in three places, all of them visible on
|
|
146
|
+
its npm page: the "pin versions explicitly" example named `core@^1.0.12` and
|
|
147
|
+
`angular@^1.2.3`, nine public exports (`FieldInputProps`, `DynamicFormOptions`,
|
|
148
|
+
`FieldTypeKey`, `LayoutConfig`, `ColumnLayoutConfig`, `RowLayoutConfig`,
|
|
149
|
+
`GridLayoutConfig`, `BaseLayoutConfig`, `ResponsiveLayoutConfig`) appeared
|
|
150
|
+
nowhere in it, and a section titled "Angular 14 and earlier" named versions the
|
|
151
|
+
package cannot run on — its peer floor is `>=14`.
|
|
152
|
+
|
|
153
|
+
Its `sideEffects: false` was also untrue: `defaultLayouts.ts` registers the
|
|
154
|
+
three layout components with a module-scope call, which is exactly the side
|
|
155
|
+
effect react and vue list their own layout modules for. The flag is now
|
|
156
|
+
`["**/fesm2022/*.mjs"]` — a glob because ng-packagr copies the field into
|
|
157
|
+
`dist/package.json`, where paths resolve one directory lower, and a bundler
|
|
158
|
+
reads whichever manifest is nearest the module. Nothing observable changes
|
|
159
|
+
today: bundling the published 1.4.0 with esbuild while importing only
|
|
160
|
+
`DynamicInput` already kept all three `register()` calls, because the fesm2022
|
|
161
|
+
bundle is a single module the app is using. The flag was a claim waiting to
|
|
162
|
+
break.
|
|
163
|
+
|
|
164
|
+
## 1.4.0
|
|
165
|
+
|
|
166
|
+
### Minor Changes
|
|
167
|
+
|
|
168
|
+
- 132f74b: Framework adapters: validation support, scoped field registries via dependency
|
|
169
|
+
injection, and unified cross-framework layout types.
|
|
170
|
+
|
|
171
|
+
- Validation wired through `DynamicInput` / `FieldInput` / `MultiFieldInput`.
|
|
172
|
+
- Scoped `FieldRegistry` injection (React context, Vue provide/inject, Angular
|
|
173
|
+
`FIELD_REGISTRY` token) so consumers can supply their own registry.
|
|
174
|
+
- Shared layout type definitions aligned across React, Vue, and Angular.
|
|
175
|
+
|
|
176
|
+
Requires `@dynamic-field-kit/core@^1.3.0` (peer dependency) for the new
|
|
177
|
+
validation and layout APIs.
|
|
178
|
+
|
|
179
|
+
Note: `@dynamic-field-kit/core` is a peer dependency (not bundled). Consumers
|
|
180
|
+
must install it alongside the adapter.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vannt-dev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -4,7 +4,10 @@ React adapter for `@dynamic-field-kit/core`.
|
|
|
4
4
|
|
|
5
5
|
This package provides React components for rendering `FieldDescription[]` and exports a React-typed `fieldRegistry`, so registered renderers can be used directly as JSX components.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Live demo: https://vannt-dev.github.io/dynamic-field-kit/react/ — plus
|
|
8
|
+
[enterprise features](https://vannt-dev.github.io/dynamic-field-kit/react/new-features/)
|
|
9
|
+
(`useDynamicForm`, HTML5 renderers, blur wiring, DevTools) and a
|
|
10
|
+
[multi-step wizard](https://vannt-dev.github.io/dynamic-field-kit/react/wizard/).
|
|
8
11
|
|
|
9
12
|
## Install
|
|
10
13
|
|
|
@@ -12,22 +15,41 @@ Demo app: https://github.com/vannt-dev/dynamic-field-kit-demo
|
|
|
12
15
|
npm install @dynamic-field-kit/core @dynamic-field-kit/react react
|
|
13
16
|
```
|
|
14
17
|
|
|
15
|
-
Note:
|
|
16
|
-
|
|
17
|
-
- Install with core: `npm install @dynamic-field-kit/core @dynamic-field-kit/react`
|
|
18
|
+
Note: `@dynamic-field-kit/core`, `react`, and `react-dom` are **peer dependencies** — this adapter does not bundle or auto-install them, so add them to your app explicitly (as shown above). Keep a single `@dynamic-field-kit/core` version across all adapters so they share one registry.
|
|
18
19
|
|
|
19
20
|
## Exports
|
|
20
21
|
|
|
21
22
|
- `DynamicInput`
|
|
22
23
|
- `FieldInput`
|
|
23
24
|
- `MultiFieldInput`
|
|
25
|
+
- `useDynamicForm`
|
|
26
|
+
- `DynamicFormDevTools`
|
|
24
27
|
- `layoutRegistry`
|
|
25
28
|
- `fieldRegistry`
|
|
29
|
+
- `FieldRegistry` (class, for scoped registries)
|
|
30
|
+
- `FieldRegistryProvider` / `useFieldRegistry` / `FieldRegistryProviderProps`
|
|
26
31
|
- `ReactFieldRenderer`
|
|
27
32
|
- `ReactFieldRegistry`
|
|
28
33
|
- `FieldDescription`
|
|
29
34
|
- `FieldTypeKey`
|
|
30
35
|
- `FieldRendererProps`
|
|
36
|
+
- `LayoutConfig`
|
|
37
|
+
- `defaultRenderersMap` / `getDefaultRenderer`
|
|
38
|
+
|
|
39
|
+
Re-exported from `@dynamic-field-kit/core` so a consumer app rarely has to import
|
|
40
|
+
both packages:
|
|
41
|
+
|
|
42
|
+
- `validateField` / `validateFieldAsync` — one field, returns `string[]`
|
|
43
|
+
- `validateFields` / `validateFieldsAsync` — a whole schema, returns `ValidationResult`
|
|
44
|
+
- `resolveDisabled` / `resolveReadOnly` / `resolveOptions` — resolve a field's dynamic conditions and options
|
|
45
|
+
- `validators` — the built-in validator helpers (`required`, `email`, `minLength`, `compose`, …)
|
|
46
|
+
- `ValidationResult`
|
|
47
|
+
|
|
48
|
+
`useDynamicForm` validates **synchronously** via `validateFields`, including on
|
|
49
|
+
submit. Fields whose `validate` hook returns a Promise are treated as valid on
|
|
50
|
+
that path, so run async rules through `validateFieldsAsync` yourself. See the
|
|
51
|
+
[core README](https://github.com/vannt-dev/dynamic-field-kit/tree/develop/packages/core#sync-vs-async-validation)
|
|
52
|
+
for the full rules.
|
|
31
53
|
|
|
32
54
|
`FieldGroupInput` (repeatable field groups) is used internally by `FieldInput` and doesn't need to be imported directly - see "Repeatable field groups" below.
|
|
33
55
|
|
|
@@ -94,6 +116,91 @@ export function Example() {
|
|
|
94
116
|
}
|
|
95
117
|
```
|
|
96
118
|
|
|
119
|
+
## Form state (`useDynamicForm`)
|
|
120
|
+
|
|
121
|
+
Holds data, errors, touched and submission state for a set of fields. Vue's
|
|
122
|
+
composable and Angular's `createDynamicFormStore` expose the same surface.
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
import { useDynamicForm, MultiFieldInput } from '@dynamic-field-kit/react';
|
|
126
|
+
|
|
127
|
+
const form = useDynamicForm({
|
|
128
|
+
fields,
|
|
129
|
+
initialValues: { country: 'VN' },
|
|
130
|
+
validateOnBlur: true, // default
|
|
131
|
+
validateOnChange: false, // default
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
<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
|
+
/>
|
|
141
|
+
<button disabled={form.isSubmitting}>
|
|
142
|
+
{form.isSubmitting ? 'Saving…' : 'Save'}
|
|
143
|
+
</button>
|
|
144
|
+
</form>;
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
| Member | Description |
|
|
148
|
+
| ----------------------------------- | --------------------------------------------------------------------------------- |
|
|
149
|
+
| `data` | Current form data, with `computeValue` fields applied |
|
|
150
|
+
| `errors` | `Record<string, string[]>`, keyed like `validateFields` |
|
|
151
|
+
| `isValid` / `isDirty` | No errors recorded / any value has changed |
|
|
152
|
+
| `isSubmitting` / `isSubmitted` | In-flight submit / at least one submit attempted |
|
|
153
|
+
| `touched` | Fields that have been blurred |
|
|
154
|
+
| `handleChange(data)` | Replace the whole form data — pass to `MultiFieldInput`'s `onChange` |
|
|
155
|
+
| `setFieldValue(name, value)` | Change one field |
|
|
156
|
+
| `handleBlur(name)` | Mark touched, and validate when `validateOnBlur` |
|
|
157
|
+
| `setFieldTouched(name, value?)` | Set touched explicitly |
|
|
158
|
+
| `setData` | Raw state setter, for escape hatches |
|
|
159
|
+
| `validate()` | Validate now, returns a boolean |
|
|
160
|
+
| `reset(values?)` | Back to `initialValues` (or the values given), clearing errors/touched/submission |
|
|
161
|
+
| `handleSubmit(onValid, onInvalid?)` | Returns a submit handler; calls `preventDefault`, validates, then dispatches |
|
|
162
|
+
|
|
163
|
+
`MultiFieldInput` tracks touched internally regardless; `onBlurField` is the
|
|
164
|
+
hook for driving an external store like this one.
|
|
165
|
+
|
|
166
|
+
## Default renderers
|
|
167
|
+
|
|
168
|
+
`text` · `number` · `password` · `email` · `textarea` · `checkbox` · `select` ·
|
|
169
|
+
`radio` · `range` · `file` · `date` · `time` · `datetime-local` · `switch`
|
|
170
|
+
|
|
171
|
+
Any type you have not registered falls back to one of these. Reach the map
|
|
172
|
+
directly if you need to wrap or inspect a default:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import {
|
|
176
|
+
defaultRenderersMap,
|
|
177
|
+
getDefaultRenderer,
|
|
178
|
+
} from '@dynamic-field-kit/react';
|
|
179
|
+
|
|
180
|
+
const Base = getDefaultRenderer('date'); // undefined for an unknown type
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`file` emits a `File` (or `File[]` when `multiple` is set), `range` and `number`
|
|
184
|
+
emit numbers, `checkbox` / `switch` emit booleans; everything else emits strings.
|
|
185
|
+
|
|
186
|
+
## DevTools
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
import { DynamicFormDevTools } from '@dynamic-field-kit/react';
|
|
190
|
+
|
|
191
|
+
<DynamicFormDevTools
|
|
192
|
+
data={form.data}
|
|
193
|
+
errors={form.errors}
|
|
194
|
+
touched={form.touched}
|
|
195
|
+
isDirty={form.isDirty}
|
|
196
|
+
fields={fields}
|
|
197
|
+
position="bottom-right" // or "bottom-left"
|
|
198
|
+
/>;
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
A floating overlay with data / errors / meta / fields tabs. The collapsed
|
|
202
|
+
button carries a red badge with the number of fields in error.
|
|
203
|
+
|
|
97
204
|
## Layouts
|
|
98
205
|
|
|
99
206
|
Use a layout name:
|
|
@@ -151,6 +258,36 @@ const fields: FieldDescription[] = [
|
|
|
151
258
|
];
|
|
152
259
|
```
|
|
153
260
|
|
|
261
|
+
## Validation & conditions
|
|
262
|
+
|
|
263
|
+
Declare a `validate` hook and dynamic `disabledCondition`/`readOnlyCondition`;
|
|
264
|
+
your renderer receives `error`, `disabled`, and `readOnly`. `MultiFieldInput`
|
|
265
|
+
emits `onValidityChange`:
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
<MultiFieldInput
|
|
269
|
+
fieldDescriptions={fields}
|
|
270
|
+
properties={data}
|
|
271
|
+
onChange={setData}
|
|
272
|
+
onValidityChange={({ valid, errors }) => setCanSubmit(valid)}
|
|
273
|
+
/>
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Read the props inside a renderer:
|
|
277
|
+
|
|
278
|
+
```tsx
|
|
279
|
+
fieldRegistry.register('text', ({ value, onValueChange, error, disabled }) => (
|
|
280
|
+
<label>
|
|
281
|
+
<input
|
|
282
|
+
disabled={disabled}
|
|
283
|
+
value={value ?? ''}
|
|
284
|
+
onChange={(e) => onValueChange?.(e.target.value)}
|
|
285
|
+
/>
|
|
286
|
+
{error && <span className="error">{[].concat(error).join(', ')}</span>}
|
|
287
|
+
</label>
|
|
288
|
+
));
|
|
289
|
+
```
|
|
290
|
+
|
|
154
291
|
## Repeatable field groups
|
|
155
292
|
|
|
156
293
|
A field with `fields` renders as a repeatable group: `data[name]` becomes an array of items, each shaped by the nested `fields`, with "Add"/"Remove" controls rendered automatically.
|
|
@@ -166,6 +303,7 @@ const fields: FieldDescription[] = [
|
|
|
166
303
|
{ name: 'phone', type: 'text', label: 'Phone' },
|
|
167
304
|
],
|
|
168
305
|
defaultItem: { email: '', phone: '' },
|
|
306
|
+
keyField: 'id', // optional: stable list key instead of the array index
|
|
169
307
|
minItems: 1,
|
|
170
308
|
maxItems: 5,
|
|
171
309
|
},
|
|
@@ -174,6 +312,25 @@ const fields: FieldDescription[] = [
|
|
|
174
312
|
<MultiFieldInput fieldDescriptions={fields} />;
|
|
175
313
|
```
|
|
176
314
|
|
|
315
|
+
## Scoped registries
|
|
316
|
+
|
|
317
|
+
`fieldRegistry` is a process-wide singleton. To give a subtree its own renderers, create an isolated `FieldRegistry` and wrap the subtree in `FieldRegistryProvider`. Anything not wrapped keeps using the global singleton.
|
|
318
|
+
|
|
319
|
+
```tsx
|
|
320
|
+
import {
|
|
321
|
+
FieldRegistry,
|
|
322
|
+
FieldRegistryProvider,
|
|
323
|
+
MultiFieldInput,
|
|
324
|
+
} from '@dynamic-field-kit/react';
|
|
325
|
+
|
|
326
|
+
const registry = new FieldRegistry();
|
|
327
|
+
registry.register('text', MyTextRenderer);
|
|
328
|
+
|
|
329
|
+
<FieldRegistryProvider registry={registry}>
|
|
330
|
+
<MultiFieldInput fieldDescriptions={fields} />
|
|
331
|
+
</FieldRegistryProvider>;
|
|
332
|
+
```
|
|
333
|
+
|
|
177
334
|
## Type augmentation
|
|
178
335
|
|
|
179
336
|
Add your app's field types through module augmentation:
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React, { ReactNode, ComponentType } from 'react';
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
export { FieldDescription, FieldRendererProps, FieldTypeKey } from '@dynamic-field-kit/core';
|
|
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';
|
|
5
4
|
|
|
6
5
|
type LayoutRenderer<C = unknown> = (props: {
|
|
7
6
|
children: React.ReactNode;
|
|
@@ -14,43 +13,106 @@ declare class LayoutRegistry {
|
|
|
14
13
|
}
|
|
15
14
|
declare const layoutRegistry: LayoutRegistry;
|
|
16
15
|
|
|
17
|
-
type BaseLayout = 'column' | 'row' | {
|
|
18
|
-
type: 'grid';
|
|
19
|
-
columns?: number;
|
|
20
|
-
gap?: number;
|
|
21
|
-
};
|
|
22
|
-
type LayoutConfig = BaseLayout | {
|
|
23
|
-
type: 'responsive';
|
|
24
|
-
mobile: BaseLayout;
|
|
25
|
-
desktop: BaseLayout;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
16
|
interface Props$2<T extends FieldTypeKey> {
|
|
29
17
|
type: T;
|
|
30
18
|
value?: unknown;
|
|
31
19
|
onChange?: (value: unknown) => void;
|
|
20
|
+
onBlur?: () => void;
|
|
32
21
|
label?: string;
|
|
33
22
|
options?: Properties[];
|
|
34
23
|
className?: string;
|
|
35
24
|
description?: ReactNode;
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
readOnly?: boolean;
|
|
27
|
+
required?: boolean;
|
|
28
|
+
touched?: boolean;
|
|
29
|
+
dirty?: boolean;
|
|
30
|
+
error?: string | string[];
|
|
31
|
+
id?: string;
|
|
32
|
+
ariaInvalid?: boolean;
|
|
33
|
+
ariaDescribedBy?: string;
|
|
34
|
+
ariaRequired?: boolean;
|
|
35
|
+
/** Extra, framework-agnostic props forwarded verbatim to the renderer. */
|
|
36
|
+
extraProps?: Properties;
|
|
36
37
|
}
|
|
37
|
-
declare const DynamicInputInner: <T extends FieldTypeKey>({ type, value, onChange, label, options, className, description, }: Props$2<T>) =>
|
|
38
|
+
declare const DynamicInputInner: <T extends FieldTypeKey>({ type, value, onChange, onBlur, label, options, className, description, disabled, readOnly, required, touched, dirty, error, id, ariaInvalid, ariaDescribedBy, ariaRequired, extraProps, }: Props$2<T>) => React.JSX.Element;
|
|
38
39
|
declare const DynamicInput: typeof DynamicInputInner;
|
|
39
40
|
|
|
40
41
|
interface Props$1 {
|
|
41
42
|
fieldDescription: FieldDescription;
|
|
42
43
|
renderInfos: Properties;
|
|
44
|
+
rootData?: Properties;
|
|
45
|
+
touched?: boolean;
|
|
46
|
+
dirty?: boolean;
|
|
47
|
+
onBlurField?: (key: string) => void;
|
|
43
48
|
onValueChangeField: (value: unknown, key: string) => void;
|
|
44
49
|
}
|
|
45
|
-
declare const FieldInput: React.MemoExoticComponent<({ fieldDescription, renderInfos, onValueChangeField, }: Props$1) =>
|
|
50
|
+
declare const FieldInput: React.MemoExoticComponent<({ fieldDescription, renderInfos, rootData, touched, dirty, onBlurField, onValueChangeField, }: Props$1) => React.JSX.Element>;
|
|
46
51
|
|
|
47
52
|
interface Props {
|
|
48
53
|
fieldDescriptions: FieldDescription[];
|
|
49
54
|
properties?: Properties;
|
|
50
55
|
onChange?: (data: Properties) => void;
|
|
51
56
|
layout?: LayoutConfig;
|
|
57
|
+
/**
|
|
58
|
+
* Top-level form data, threaded down through repeatable groups so a nested
|
|
59
|
+
* field's `appearCondition`/`computeValue` can read the root form. Omitted at
|
|
60
|
+
* the top level, where the form's own data is the root.
|
|
61
|
+
*/
|
|
62
|
+
rootData?: Properties;
|
|
63
|
+
/**
|
|
64
|
+
* Called with the recursive validation result ({ valid, errors }) on every
|
|
65
|
+
* change. On the top-level component this covers the whole form (groups
|
|
66
|
+
* included).
|
|
67
|
+
*/
|
|
68
|
+
onValidityChange?: (result: ValidationResult) => void;
|
|
69
|
+
/**
|
|
70
|
+
* Called with a field's name when it loses focus. Touched state is still
|
|
71
|
+
* tracked internally either way; this is the hook for driving an external
|
|
72
|
+
* form store - pass `useDynamicForm`'s `handleBlur` to get its `touched`
|
|
73
|
+
* map and `validateOnBlur` behaviour.
|
|
74
|
+
*/
|
|
75
|
+
onBlurField?: (fieldName: string) => void;
|
|
76
|
+
}
|
|
77
|
+
declare const MultiFieldInput: ({ fieldDescriptions, properties, onChange, layout, rootData, onValidityChange, onBlurField, }: Props) => React.JSX.Element;
|
|
78
|
+
|
|
79
|
+
interface DynamicFormDevToolsProps {
|
|
80
|
+
data: Properties;
|
|
81
|
+
errors?: Record<string, string[]>;
|
|
82
|
+
touched?: Record<string, boolean>;
|
|
83
|
+
isDirty?: boolean;
|
|
84
|
+
fields?: FieldDescription[];
|
|
85
|
+
position?: 'bottom-right' | 'bottom-left';
|
|
86
|
+
}
|
|
87
|
+
declare const DynamicFormDevTools: React.FC<DynamicFormDevToolsProps>;
|
|
88
|
+
|
|
89
|
+
interface UseDynamicFormOptions {
|
|
90
|
+
fields: FieldDescription[];
|
|
91
|
+
initialValues?: Properties;
|
|
92
|
+
validateOnBlur?: boolean;
|
|
93
|
+
validateOnChange?: boolean;
|
|
52
94
|
}
|
|
53
|
-
|
|
95
|
+
interface UseDynamicFormResult {
|
|
96
|
+
data: Properties;
|
|
97
|
+
errors: Record<string, string[]>;
|
|
98
|
+
isValid: boolean;
|
|
99
|
+
isDirty: boolean;
|
|
100
|
+
isSubmitting: boolean;
|
|
101
|
+
isSubmitted: boolean;
|
|
102
|
+
touched: Record<string, boolean>;
|
|
103
|
+
setData: React.Dispatch<React.SetStateAction<Properties>>;
|
|
104
|
+
setFieldValue: (name: string, value: unknown) => void;
|
|
105
|
+
setFieldTouched: (name: string, isTouched?: boolean) => void;
|
|
106
|
+
handleChange: (newData: Properties) => void;
|
|
107
|
+
handleBlur: (fieldName: string) => void;
|
|
108
|
+
reset: (newValues?: Properties) => void;
|
|
109
|
+
validate: () => boolean;
|
|
110
|
+
handleSubmit: (onValid: (data: Properties) => void | Promise<void>, onInvalid?: (errors: Record<string, string[]>) => void) => (e?: React.FormEvent) => Promise<void>;
|
|
111
|
+
}
|
|
112
|
+
declare function useDynamicForm({ fields, initialValues, validateOnBlur, validateOnChange, }: UseDynamicFormOptions): UseDynamicFormResult;
|
|
113
|
+
|
|
114
|
+
declare const defaultRenderersMap: Record<string, React.FC<FieldRendererProps>>;
|
|
115
|
+
declare function getDefaultRenderer(type: string): React.FC<FieldRendererProps> | undefined;
|
|
54
116
|
|
|
55
117
|
type ReactFieldRenderer<T = unknown> = ComponentType<FieldRendererProps<T>>;
|
|
56
118
|
interface ReactFieldRegistry {
|
|
@@ -59,4 +121,12 @@ interface ReactFieldRegistry {
|
|
|
59
121
|
}
|
|
60
122
|
declare const fieldRegistry: ReactFieldRegistry;
|
|
61
123
|
|
|
62
|
-
|
|
124
|
+
interface FieldRegistryProviderProps {
|
|
125
|
+
registry: ReactFieldRegistry;
|
|
126
|
+
children: React.ReactNode;
|
|
127
|
+
}
|
|
128
|
+
declare const FieldRegistryProvider: ({ registry, children, }: FieldRegistryProviderProps) => React.ReactElement;
|
|
129
|
+
/** The registry for the nearest provider, or the global singleton. */
|
|
130
|
+
declare function useFieldRegistry(): ReactFieldRegistry;
|
|
131
|
+
|
|
132
|
+
export { DynamicFormDevTools, DynamicInput, FieldInput, FieldRegistryProvider, type FieldRegistryProviderProps, MultiFieldInput, type ReactFieldRegistry, type ReactFieldRenderer, defaultRenderersMap, fieldRegistry, getDefaultRenderer, layoutRegistry, useDynamicForm, useFieldRegistry };
|