@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 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: Core is shared runtime. Install core separately and ensure a single version is used across adapters to avoid duplicate registries.
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 = any> = (props: {
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?: any;
31
- onChange?: (value: any) => void;
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 DynamicInput: <T extends FieldTypeKey>({ type, value, onChange, label, options, className, description, }: Props$2<T>) => react_jsx_runtime.JSX.Element;
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
- onValueChangeField: (value: any, key: string) => void;
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 = any> = ComponentType<FieldRendererProps<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
- export { DynamicInput, FieldInput, type LayoutConfig, MultiFieldInput, type ReactFieldRegistry, type ReactFieldRenderer, fieldRegistry, layoutRegistry };
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 = any> = (props: {
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?: any;
31
- onChange?: (value: any) => void;
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 DynamicInput: <T extends FieldTypeKey>({ type, value, onChange, label, options, className, description, }: Props$2<T>) => react_jsx_runtime.JSX.Element;
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
- onValueChangeField: (value: any, key: string) => void;
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 = any> = ComponentType<FieldRendererProps<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
- export { DynamicInput, FieldInput, type LayoutConfig, MultiFieldInput, type ReactFieldRegistry, type ReactFieldRenderer, fieldRegistry, layoutRegistry };
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 };