React adapter for @dynamic-field-kit/core.
This package provides React components for rendering FieldDescription[] and exports a React-typed fieldRegistry, so registered renderers can be used directly as JSX components.
Live demo: https://vannt-dev.github.io/dynamic-field-kit/react/
npm install @dynamic-field-kit/core @dynamic-field-kit/react reactNote: @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.
DynamicInputFieldInputMultiFieldInputuseDynamicFormDynamicFormDevToolslayoutRegistryfieldRegistryFieldRegistry(class, for scoped registries)FieldRegistryProvider/useFieldRegistry/FieldRegistryProviderPropsReactFieldRendererReactFieldRegistryFieldDescriptionFieldTypeKeyFieldRendererPropsLayoutConfigvalidateField/validateFields/resolveDisabled/resolveReadOnly/ValidationResultdefaultRenderersMap/getDefaultRenderer
FieldGroupInput (repeatable field groups) is used internally by FieldInput and doesn't need to be imported directly - see "Repeatable field groups" below.
Default layouts are registered automatically when you import the package root.
Built-in layouts:
columnrowgridresponsive
Register React components or function components through the React adapter:
import { fieldRegistry } from '@dynamic-field-kit/react';
fieldRegistry.register('text', ({ value, onValueChange, label }) => (
<label style={{ display: 'grid', gap: 4 }}>
<span>{label}</span>
<input
value={value ?? ''}
onChange={(e) => onValueChange?.(e.target.value)}
/>
</label>
));
fieldRegistry.register('number', ({ value, onValueChange, label }) => (
<label style={{ display: 'grid', gap: 4 }}>
<span>{label}</span>
<input
type="number"
value={value ?? ''}
onChange={(e) => onValueChange?.(Number(e.target.value))}
/>
</label>
));import { useState } from 'react';
import { MultiFieldInput } from '@dynamic-field-kit/react';
import type { FieldDescription } from '@dynamic-field-kit/core';
const fields: FieldDescription[] = [
{ name: 'name', type: 'text', label: 'Name' },
{ name: 'age', type: 'number', label: 'Age' },
];
export function Example() {
const [data, setData] = useState({});
return (
<MultiFieldInput
fieldDescriptions={fields}
properties={data}
onChange={setData}
/>
);
}Holds data, errors, touched and submission state for a set of fields. Vue's
composable and Angular's createDynamicFormStore expose the same surface.
import { useDynamicForm, MultiFieldInput } from '@dynamic-field-kit/react';
const form = useDynamicForm({
fields,
initialValues: { country: 'VN' },
validateOnBlur: true, // default
validateOnChange: false, // default
});
<form onSubmit={form.handleSubmit((data) => save(data))}>
<MultiFieldInput
fieldDescriptions={fields}
properties={form.data}
onChange={form.handleChange}
onBlurField={form.handleBlur} // wires touched + validateOnBlur
/>
<button disabled={form.isSubmitting}>
{form.isSubmitting ? 'Saving…' : 'Save'}
</button>
</form>;| Member | Description |
|---|---|
data |
Current form data, with computeValue fields applied |
errors |
Record<string, string[]>, keyed like validateFields |
isValid / isDirty |
No errors recorded / any value has changed |
isSubmitting / isSubmitted |
In-flight submit / at least one submit attempted |
touched |
Fields that have been blurred |
handleChange(data) |
Replace the whole form data — pass to MultiFieldInput's onChange |
setFieldValue(name, value) |
Change one field |
handleBlur(name) |
Mark touched, and validate when validateOnBlur |
setFieldTouched(name, value?) |
Set touched explicitly |
setData |
Raw state setter, for escape hatches |
validate() |
Validate now, returns a boolean |
reset(values?) |
Back to initialValues (or the values given), clearing errors/touched/submission |
handleSubmit(onValid, onInvalid?) |
Returns a submit handler; calls preventDefault, validates, then dispatches |
MultiFieldInput tracks touched internally regardless; onBlurField is the
hook for driving an external store like this one.
text · number · password · email · textarea · checkbox · select ·
radio · range · file · date · time · datetime-local · switch
Any type you have not registered falls back to one of these. Reach the map directly if you need to wrap or inspect a default:
import {
defaultRenderersMap,
getDefaultRenderer,
} from '@dynamic-field-kit/react';
const Base = getDefaultRenderer('date'); // undefined for an unknown typefile emits a File (or File[] when multiple is set), range and number
emit numbers, checkbox / switch emit booleans; everything else emits strings.
import { DynamicFormDevTools } from '@dynamic-field-kit/react';
<DynamicFormDevTools
data={form.data}
errors={form.errors}
touched={form.touched}
isDirty={form.isDirty}
fields={fields}
position="bottom-right" // or "bottom-left"
/>;A floating overlay with data / errors / meta / fields tabs. The collapsed button carries a red badge with the number of fields in error.
Use a layout name:
<MultiFieldInput fieldDescriptions={fields} layout="grid" />Use a layout config object:
<MultiFieldInput
fieldDescriptions={fields}
layout={{ type: 'grid', columns: 3, gap: 16 }}
/>Use the built-in responsive layout:
<MultiFieldInput
fieldDescriptions={fields}
layout={{
type: 'responsive',
mobile: 'column',
desktop: { type: 'grid', columns: 2, gap: 12 },
}}
/>Register a custom layout:
import { layoutRegistry } from '@dynamic-field-kit/react';
layoutRegistry.register('stack-tight', ({ children }) => (
<div style={{ display: 'grid', gap: 8 }}>{children}</div>
));Give a field a computeValue to derive its value from the rest of the form data whenever any field changes:
const fields: FieldDescription[] = [
{ name: 'firstName', type: 'text' },
{ name: 'lastName', type: 'text' },
{
name: 'fullName',
type: 'text',
computeValue: (data) =>
`${data.firstName ?? ''} ${data.lastName ?? ''}`.trim(),
},
];Declare a validate hook and dynamic disabledCondition/readOnlyCondition;
your renderer receives error, disabled, and readOnly. MultiFieldInput
emits onValidityChange:
<MultiFieldInput
fieldDescriptions={fields}
properties={data}
onChange={setData}
onValidityChange={({ valid, errors }) => setCanSubmit(valid)}
/>Read the props inside a renderer:
fieldRegistry.register('text', ({ value, onValueChange, error, disabled }) => (
<label>
<input
disabled={disabled}
value={value ?? ''}
onChange={(e) => onValueChange?.(e.target.value)}
/>
{error && <span className="error">{[].concat(error).join(', ')}</span>}
</label>
));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.
const fields: FieldDescription[] = [
{
name: 'contacts',
type: 'group',
label: 'Contacts',
fields: [
{ name: 'email', type: 'text', label: 'Email' },
{ name: 'phone', type: 'text', label: 'Phone' },
],
defaultItem: { email: '', phone: '' },
keyField: 'id', // optional: stable list key instead of the array index
minItems: 1,
maxItems: 5,
},
];
<MultiFieldInput fieldDescriptions={fields} />;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.
import {
FieldRegistry,
FieldRegistryProvider,
MultiFieldInput,
} from '@dynamic-field-kit/react';
const registry = new FieldRegistry();
registry.register('text', MyTextRenderer);
<FieldRegistryProvider registry={registry}>
<MultiFieldInput fieldDescriptions={fields} />
</FieldRegistryProvider>;Add your app's field types through module augmentation:
import '@dynamic-field-kit/core';
declare module '@dynamic-field-kit/core' {
interface FieldTypeMap {
text: string;
number: number;
}
}@dynamic-field-kit/corestays framework-agnostic and does not export React-specific JSX types.@dynamic-field-kit/reactnarrows the shared registry to React component types sofieldRegistry.get(type)can be rendered safely in TSX.MultiFieldInputfilters fields usingappearConditionand derives fields usingcomputeValue.DynamicInputrendersUnknown field type: ...when a renderer is missing.- Fields with
fieldsrender as repeatable groups instead of going throughfieldRegistry.
MIT