Skip to content

Latest commit

 

History

History
342 lines (268 loc) · 10.2 KB

File metadata and controls

342 lines (268 loc) · 10.2 KB

@dynamic-field-kit/react

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/

Install

npm install @dynamic-field-kit/core @dynamic-field-kit/react react

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.

Exports

  • DynamicInput
  • FieldInput
  • MultiFieldInput
  • useDynamicForm
  • DynamicFormDevTools
  • layoutRegistry
  • fieldRegistry
  • FieldRegistry (class, for scoped registries)
  • FieldRegistryProvider / useFieldRegistry / FieldRegistryProviderProps
  • ReactFieldRenderer
  • ReactFieldRegistry
  • FieldDescription
  • FieldTypeKey
  • FieldRendererProps
  • LayoutConfig
  • validateField / validateFields / resolveDisabled / resolveReadOnly / ValidationResult
  • defaultRenderersMap / 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:

  • column
  • row
  • grid
  • responsive

Register field renderers

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>
));

Basic usage

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}
    />
  );
}

Form state (useDynamicForm)

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.

Default renderers

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 type

file emits a File (or File[] when multiple is set), range and number emit numbers, checkbox / switch emit booleans; everything else emits strings.

DevTools

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.

Layouts

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>
));

Derived fields with computeValue

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(),
  },
];

Validation & conditions

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>
));

Repeatable field groups

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} />;

Scoped registries

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>;

Type augmentation

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;
  }
}

Notes

  • @dynamic-field-kit/core stays framework-agnostic and does not export React-specific JSX types.
  • @dynamic-field-kit/react narrows the shared registry to React component types so fieldRegistry.get(type) can be rendered safely in TSX.
  • MultiFieldInput filters fields using appearCondition and derives fields using computeValue.
  • DynamicInput renders Unknown field type: ... when a renderer is missing.
  • Fields with fields render as repeatable groups instead of going through fieldRegistry.

License

MIT