@dynamic-field-kit/react 1.2.0 → 1.4.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/README.md +99 -4
- package/dist/index.d.mts +49 -22
- package/dist/index.d.ts +49 -22
- package/dist/index.js +332 -47
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +344 -43
- package/dist/index.mjs.map +1 -1
- package/package.json +25 -9
package/README.md
CHANGED
|
@@ -12,9 +12,7 @@ Demo app: https://github.com/vannt-dev/dynamic-field-kit-demo
|
|
|
12
12
|
npm install @dynamic-field-kit/core @dynamic-field-kit/react react
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
Note:
|
|
16
|
-
|
|
17
|
-
- Install with core: `npm install @dynamic-field-kit/core @dynamic-field-kit/react`
|
|
15
|
+
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
16
|
|
|
19
17
|
## Exports
|
|
20
18
|
|
|
@@ -23,11 +21,17 @@ Note: Core is shared runtime. Install core separately and ensure a single versio
|
|
|
23
21
|
- `MultiFieldInput`
|
|
24
22
|
- `layoutRegistry`
|
|
25
23
|
- `fieldRegistry`
|
|
24
|
+
- `FieldRegistry` (class, for scoped registries)
|
|
25
|
+
- `FieldRegistryProvider` / `useFieldRegistry` / `FieldRegistryProviderProps`
|
|
26
26
|
- `ReactFieldRenderer`
|
|
27
27
|
- `ReactFieldRegistry`
|
|
28
28
|
- `FieldDescription`
|
|
29
29
|
- `FieldTypeKey`
|
|
30
30
|
- `FieldRendererProps`
|
|
31
|
+
- `LayoutConfig`
|
|
32
|
+
- `validateField` / `validateFields` / `resolveDisabled` / `resolveReadOnly` / `ValidationResult`
|
|
33
|
+
|
|
34
|
+
`FieldGroupInput` (repeatable field groups) is used internally by `FieldInput` and doesn't need to be imported directly - see "Repeatable field groups" below.
|
|
31
35
|
|
|
32
36
|
Default layouts are registered automatically when you import the package root.
|
|
33
37
|
|
|
@@ -132,6 +136,96 @@ layoutRegistry.register('stack-tight', ({ children }) => (
|
|
|
132
136
|
));
|
|
133
137
|
```
|
|
134
138
|
|
|
139
|
+
## Derived fields with `computeValue`
|
|
140
|
+
|
|
141
|
+
Give a field a `computeValue` to derive its value from the rest of the form data whenever any field changes:
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
const fields: FieldDescription[] = [
|
|
145
|
+
{ name: 'firstName', type: 'text' },
|
|
146
|
+
{ name: 'lastName', type: 'text' },
|
|
147
|
+
{
|
|
148
|
+
name: 'fullName',
|
|
149
|
+
type: 'text',
|
|
150
|
+
computeValue: (data) =>
|
|
151
|
+
`${data.firstName ?? ''} ${data.lastName ?? ''}`.trim(),
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Validation & conditions
|
|
157
|
+
|
|
158
|
+
Declare a `validate` hook and dynamic `disabledCondition`/`readOnlyCondition`;
|
|
159
|
+
your renderer receives `error`, `disabled`, and `readOnly`. `MultiFieldInput`
|
|
160
|
+
emits `onValidityChange`:
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
<MultiFieldInput
|
|
164
|
+
fieldDescriptions={fields}
|
|
165
|
+
properties={data}
|
|
166
|
+
onChange={setData}
|
|
167
|
+
onValidityChange={({ valid, errors }) => setCanSubmit(valid)}
|
|
168
|
+
/>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Read the props inside a renderer:
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
fieldRegistry.register('text', ({ value, onValueChange, error, disabled }) => (
|
|
175
|
+
<label>
|
|
176
|
+
<input
|
|
177
|
+
disabled={disabled}
|
|
178
|
+
value={value ?? ''}
|
|
179
|
+
onChange={(e) => onValueChange?.(e.target.value)}
|
|
180
|
+
/>
|
|
181
|
+
{error && <span className="error">{[].concat(error).join(', ')}</span>}
|
|
182
|
+
</label>
|
|
183
|
+
));
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Repeatable field groups
|
|
187
|
+
|
|
188
|
+
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.
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
const fields: FieldDescription[] = [
|
|
192
|
+
{
|
|
193
|
+
name: 'contacts',
|
|
194
|
+
type: 'group',
|
|
195
|
+
label: 'Contacts',
|
|
196
|
+
fields: [
|
|
197
|
+
{ name: 'email', type: 'text', label: 'Email' },
|
|
198
|
+
{ name: 'phone', type: 'text', label: 'Phone' },
|
|
199
|
+
],
|
|
200
|
+
defaultItem: { email: '', phone: '' },
|
|
201
|
+
keyField: 'id', // optional: stable list key instead of the array index
|
|
202
|
+
minItems: 1,
|
|
203
|
+
maxItems: 5,
|
|
204
|
+
},
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
<MultiFieldInput fieldDescriptions={fields} />;
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Scoped registries
|
|
211
|
+
|
|
212
|
+
`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.
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
import {
|
|
216
|
+
FieldRegistry,
|
|
217
|
+
FieldRegistryProvider,
|
|
218
|
+
MultiFieldInput,
|
|
219
|
+
} from '@dynamic-field-kit/react';
|
|
220
|
+
|
|
221
|
+
const registry = new FieldRegistry();
|
|
222
|
+
registry.register('text', MyTextRenderer);
|
|
223
|
+
|
|
224
|
+
<FieldRegistryProvider registry={registry}>
|
|
225
|
+
<MultiFieldInput fieldDescriptions={fields} />
|
|
226
|
+
</FieldRegistryProvider>;
|
|
227
|
+
```
|
|
228
|
+
|
|
135
229
|
## Type augmentation
|
|
136
230
|
|
|
137
231
|
Add your app's field types through module augmentation:
|
|
@@ -151,8 +245,9 @@ declare module '@dynamic-field-kit/core' {
|
|
|
151
245
|
|
|
152
246
|
- `@dynamic-field-kit/core` stays framework-agnostic and does not export React-specific JSX types.
|
|
153
247
|
- `@dynamic-field-kit/react` narrows the shared registry to React component types so `fieldRegistry.get(type)` can be rendered safely in TSX.
|
|
154
|
-
- `MultiFieldInput` filters fields using `appearCondition`.
|
|
248
|
+
- `MultiFieldInput` filters fields using `appearCondition` and derives fields using `computeValue`.
|
|
155
249
|
- `DynamicInput` renders `Unknown field type: ...` when a renderer is missing.
|
|
250
|
+
- Fields with `fields` render as repeatable groups instead of going through `fieldRegistry`.
|
|
156
251
|
|
|
157
252
|
## License
|
|
158
253
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import React, { ReactNode, ComponentType } from 'react';
|
|
2
|
+
import { FieldTypeKey, Properties, FieldDescription, LayoutConfig, ValidationResult, FieldTypeMap, FieldRendererProps } 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';
|
|
2
4
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
3
|
-
import { FieldTypeKey, Properties, FieldDescription, FieldTypeMap, FieldRendererProps } from '@dynamic-field-kit/core';
|
|
4
|
-
export { FieldDescription, FieldRendererProps, FieldTypeKey } from '@dynamic-field-kit/core';
|
|
5
5
|
|
|
6
|
-
type LayoutRenderer<C =
|
|
6
|
+
type LayoutRenderer<C = unknown> = (props: {
|
|
7
7
|
children: React.ReactNode;
|
|
8
8
|
config?: C;
|
|
9
9
|
}) => React.ReactElement;
|
|
@@ -14,48 +14,75 @@ declare class LayoutRegistry {
|
|
|
14
14
|
}
|
|
15
15
|
declare const layoutRegistry: LayoutRegistry;
|
|
16
16
|
|
|
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
17
|
interface Props$2<T extends FieldTypeKey> {
|
|
29
18
|
type: T;
|
|
30
|
-
value?:
|
|
31
|
-
onChange?: (value:
|
|
19
|
+
value?: unknown;
|
|
20
|
+
onChange?: (value: unknown) => void;
|
|
21
|
+
onBlur?: () => void;
|
|
32
22
|
label?: string;
|
|
33
23
|
options?: Properties[];
|
|
34
24
|
className?: string;
|
|
35
25
|
description?: ReactNode;
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
readOnly?: boolean;
|
|
28
|
+
required?: boolean;
|
|
29
|
+
touched?: boolean;
|
|
30
|
+
dirty?: boolean;
|
|
31
|
+
error?: string | string[];
|
|
32
|
+
id?: string;
|
|
33
|
+
ariaInvalid?: boolean;
|
|
34
|
+
ariaDescribedBy?: string;
|
|
35
|
+
ariaRequired?: boolean;
|
|
36
|
+
/** Extra, framework-agnostic props forwarded verbatim to the renderer. */
|
|
37
|
+
extraProps?: Properties;
|
|
36
38
|
}
|
|
37
|
-
declare const
|
|
39
|
+
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_runtime.JSX.Element;
|
|
40
|
+
declare const DynamicInput: typeof DynamicInputInner;
|
|
38
41
|
|
|
39
42
|
interface Props$1 {
|
|
40
43
|
fieldDescription: FieldDescription;
|
|
41
44
|
renderInfos: Properties;
|
|
42
|
-
|
|
45
|
+
rootData?: Properties;
|
|
46
|
+
touched?: boolean;
|
|
47
|
+
dirty?: boolean;
|
|
48
|
+
onBlurField?: (key: string) => void;
|
|
49
|
+
onValueChangeField: (value: unknown, key: string) => void;
|
|
43
50
|
}
|
|
44
|
-
declare const FieldInput: ({ fieldDescription, renderInfos, onValueChangeField, }: Props$1) => react_jsx_runtime.JSX.Element
|
|
51
|
+
declare const FieldInput: React.MemoExoticComponent<({ fieldDescription, renderInfos, rootData, touched, dirty, onBlurField, onValueChangeField, }: Props$1) => react_jsx_runtime.JSX.Element>;
|
|
45
52
|
|
|
46
53
|
interface Props {
|
|
47
54
|
fieldDescriptions: FieldDescription[];
|
|
48
55
|
properties?: Properties;
|
|
49
56
|
onChange?: (data: Properties) => void;
|
|
50
57
|
layout?: LayoutConfig;
|
|
58
|
+
/**
|
|
59
|
+
* Top-level form data, threaded down through repeatable groups so a nested
|
|
60
|
+
* field's `appearCondition`/`computeValue` can read the root form. Omitted at
|
|
61
|
+
* the top level, where the form's own data is the root.
|
|
62
|
+
*/
|
|
63
|
+
rootData?: Properties;
|
|
64
|
+
/**
|
|
65
|
+
* Called with the recursive validation result ({ valid, errors }) on every
|
|
66
|
+
* change. On the top-level component this covers the whole form (groups
|
|
67
|
+
* included).
|
|
68
|
+
*/
|
|
69
|
+
onValidityChange?: (result: ValidationResult) => void;
|
|
51
70
|
}
|
|
52
|
-
declare const MultiFieldInput: ({ fieldDescriptions, properties, onChange, layout, }: Props) => react_jsx_runtime.JSX.Element;
|
|
71
|
+
declare const MultiFieldInput: ({ fieldDescriptions, properties, onChange, layout, rootData, onValidityChange, }: Props) => react_jsx_runtime.JSX.Element;
|
|
53
72
|
|
|
54
|
-
type ReactFieldRenderer<T =
|
|
73
|
+
type ReactFieldRenderer<T = unknown> = ComponentType<FieldRendererProps<T>>;
|
|
55
74
|
interface ReactFieldRegistry {
|
|
56
75
|
register<K extends keyof FieldTypeMap>(type: K, renderer: ReactFieldRenderer<FieldTypeMap[K]>): void;
|
|
57
76
|
get<K extends keyof FieldTypeMap>(type: K): ReactFieldRenderer<FieldTypeMap[K]> | undefined;
|
|
58
77
|
}
|
|
59
78
|
declare const fieldRegistry: ReactFieldRegistry;
|
|
60
79
|
|
|
61
|
-
|
|
80
|
+
interface FieldRegistryProviderProps {
|
|
81
|
+
registry: ReactFieldRegistry;
|
|
82
|
+
children: React.ReactNode;
|
|
83
|
+
}
|
|
84
|
+
declare const FieldRegistryProvider: ({ registry, children, }: FieldRegistryProviderProps) => React.ReactElement;
|
|
85
|
+
/** The registry for the nearest provider, or the global singleton. */
|
|
86
|
+
declare function useFieldRegistry(): ReactFieldRegistry;
|
|
87
|
+
|
|
88
|
+
export { DynamicInput, FieldInput, FieldRegistryProvider, type FieldRegistryProviderProps, MultiFieldInput, type ReactFieldRegistry, type ReactFieldRenderer, fieldRegistry, layoutRegistry, useFieldRegistry };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import React, { ReactNode, ComponentType } from 'react';
|
|
2
|
+
import { FieldTypeKey, Properties, FieldDescription, LayoutConfig, ValidationResult, FieldTypeMap, FieldRendererProps } 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';
|
|
2
4
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
3
|
-
import { FieldTypeKey, Properties, FieldDescription, FieldTypeMap, FieldRendererProps } from '@dynamic-field-kit/core';
|
|
4
|
-
export { FieldDescription, FieldRendererProps, FieldTypeKey } from '@dynamic-field-kit/core';
|
|
5
5
|
|
|
6
|
-
type LayoutRenderer<C =
|
|
6
|
+
type LayoutRenderer<C = unknown> = (props: {
|
|
7
7
|
children: React.ReactNode;
|
|
8
8
|
config?: C;
|
|
9
9
|
}) => React.ReactElement;
|
|
@@ -14,48 +14,75 @@ declare class LayoutRegistry {
|
|
|
14
14
|
}
|
|
15
15
|
declare const layoutRegistry: LayoutRegistry;
|
|
16
16
|
|
|
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
17
|
interface Props$2<T extends FieldTypeKey> {
|
|
29
18
|
type: T;
|
|
30
|
-
value?:
|
|
31
|
-
onChange?: (value:
|
|
19
|
+
value?: unknown;
|
|
20
|
+
onChange?: (value: unknown) => void;
|
|
21
|
+
onBlur?: () => void;
|
|
32
22
|
label?: string;
|
|
33
23
|
options?: Properties[];
|
|
34
24
|
className?: string;
|
|
35
25
|
description?: ReactNode;
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
readOnly?: boolean;
|
|
28
|
+
required?: boolean;
|
|
29
|
+
touched?: boolean;
|
|
30
|
+
dirty?: boolean;
|
|
31
|
+
error?: string | string[];
|
|
32
|
+
id?: string;
|
|
33
|
+
ariaInvalid?: boolean;
|
|
34
|
+
ariaDescribedBy?: string;
|
|
35
|
+
ariaRequired?: boolean;
|
|
36
|
+
/** Extra, framework-agnostic props forwarded verbatim to the renderer. */
|
|
37
|
+
extraProps?: Properties;
|
|
36
38
|
}
|
|
37
|
-
declare const
|
|
39
|
+
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_runtime.JSX.Element;
|
|
40
|
+
declare const DynamicInput: typeof DynamicInputInner;
|
|
38
41
|
|
|
39
42
|
interface Props$1 {
|
|
40
43
|
fieldDescription: FieldDescription;
|
|
41
44
|
renderInfos: Properties;
|
|
42
|
-
|
|
45
|
+
rootData?: Properties;
|
|
46
|
+
touched?: boolean;
|
|
47
|
+
dirty?: boolean;
|
|
48
|
+
onBlurField?: (key: string) => void;
|
|
49
|
+
onValueChangeField: (value: unknown, key: string) => void;
|
|
43
50
|
}
|
|
44
|
-
declare const FieldInput: ({ fieldDescription, renderInfos, onValueChangeField, }: Props$1) => react_jsx_runtime.JSX.Element
|
|
51
|
+
declare const FieldInput: React.MemoExoticComponent<({ fieldDescription, renderInfos, rootData, touched, dirty, onBlurField, onValueChangeField, }: Props$1) => react_jsx_runtime.JSX.Element>;
|
|
45
52
|
|
|
46
53
|
interface Props {
|
|
47
54
|
fieldDescriptions: FieldDescription[];
|
|
48
55
|
properties?: Properties;
|
|
49
56
|
onChange?: (data: Properties) => void;
|
|
50
57
|
layout?: LayoutConfig;
|
|
58
|
+
/**
|
|
59
|
+
* Top-level form data, threaded down through repeatable groups so a nested
|
|
60
|
+
* field's `appearCondition`/`computeValue` can read the root form. Omitted at
|
|
61
|
+
* the top level, where the form's own data is the root.
|
|
62
|
+
*/
|
|
63
|
+
rootData?: Properties;
|
|
64
|
+
/**
|
|
65
|
+
* Called with the recursive validation result ({ valid, errors }) on every
|
|
66
|
+
* change. On the top-level component this covers the whole form (groups
|
|
67
|
+
* included).
|
|
68
|
+
*/
|
|
69
|
+
onValidityChange?: (result: ValidationResult) => void;
|
|
51
70
|
}
|
|
52
|
-
declare const MultiFieldInput: ({ fieldDescriptions, properties, onChange, layout, }: Props) => react_jsx_runtime.JSX.Element;
|
|
71
|
+
declare const MultiFieldInput: ({ fieldDescriptions, properties, onChange, layout, rootData, onValidityChange, }: Props) => react_jsx_runtime.JSX.Element;
|
|
53
72
|
|
|
54
|
-
type ReactFieldRenderer<T =
|
|
73
|
+
type ReactFieldRenderer<T = unknown> = ComponentType<FieldRendererProps<T>>;
|
|
55
74
|
interface ReactFieldRegistry {
|
|
56
75
|
register<K extends keyof FieldTypeMap>(type: K, renderer: ReactFieldRenderer<FieldTypeMap[K]>): void;
|
|
57
76
|
get<K extends keyof FieldTypeMap>(type: K): ReactFieldRenderer<FieldTypeMap[K]> | undefined;
|
|
58
77
|
}
|
|
59
78
|
declare const fieldRegistry: ReactFieldRegistry;
|
|
60
79
|
|
|
61
|
-
|
|
80
|
+
interface FieldRegistryProviderProps {
|
|
81
|
+
registry: ReactFieldRegistry;
|
|
82
|
+
children: React.ReactNode;
|
|
83
|
+
}
|
|
84
|
+
declare const FieldRegistryProvider: ({ registry, children, }: FieldRegistryProviderProps) => React.ReactElement;
|
|
85
|
+
/** The registry for the nearest provider, or the global singleton. */
|
|
86
|
+
declare function useFieldRegistry(): ReactFieldRegistry;
|
|
87
|
+
|
|
88
|
+
export { DynamicInput, FieldInput, FieldRegistryProvider, type FieldRegistryProviderProps, MultiFieldInput, type ReactFieldRegistry, type ReactFieldRenderer, fieldRegistry, layoutRegistry, useFieldRegistry };
|