/** * The recursive dispatcher: classifies a single schema node and renders the * matching field component for its model kind (const/number/boolean render * inline; every other kind delegates to a dedicated field component). Object, * array, and union renderers recurse back through here for their children. */ import { memo, type ReactNode } from 'react'; import { Checkbox } from '../components/checkbox'; import { Field } from '../components/field'; import { NumberInput, TextInput } from '../components/inputs'; import { ArrayField } from './array-field'; import { classifySchema } from './classify'; import { EnumField } from './enum-field'; import { FieldGroup } from './field-group'; import type { FieldModel } from './field-model'; import { JsonField } from './json-field'; import { MultiSelectField } from './multiselect-field'; import { ObjectFields } from './object-fields'; import { RecordField } from './record-field'; import { scalarLabel } from './resolve'; import { StringField } from './string-field'; import type { JsonSchema, SchemaFormErrors } from './types'; import { UnionField } from './union-field'; interface FieldNodeProps { readonly schema: JsonSchema; readonly root: JsonSchema; readonly value: unknown; readonly onChange: (value: unknown) => void; readonly path: string; /** The label to show — a property key, array-item caption, or `undefined` at root. */ readonly label: string | undefined; readonly required: boolean; /** * Append the design system's required marker (a trailing ` *`, the same * convention host forms use on their own labels) to the heading when the * field is required. Opt-in per face: the union variant surface sets it so * the ACTIVE variant's obligations read at a glance. */ readonly markRequired?: boolean; readonly errors: SchemaFormErrors | undefined; readonly idPrefix: string; /** * Per-field picker placeholders keyed by property name, handed to an OBJECT node so * its direct `enum`-select children draw their placeholder. Carried by the root form * only; nested objects receive none. */ readonly fieldWording?: Readonly>; /** This field's own picker placeholder, when it draws as an `enum` select. */ readonly choiceLabel?: string; } function labelText(explicit: string | undefined, label: string | undefined): string { return explicit ?? label ?? 'Value'; } /** * A single schema node, dispatched on its classified model kind. Wrapped in * `memo` so a field whose props are unchanged is skipped: the containers hand * each child a referentially-stable `onChange` (see {@link useKeyedCallbacks}) * and the controlled value keeps unedited branches identity-equal, so editing * one field re-renders only that field's subtree, not every sibling. */ function FieldNodeImpl(props: FieldNodeProps): ReactNode { const { root, value, onChange, path, required, idPrefix, errors } = props; const classified = classifySchema(props.schema, root); const base = labelText(classified.title, props.label); const heading = props.markRequired === true && required ? `${base} *` : base; const { model, description } = classified; return renderField({ model, nullable: classified.nullable, heading, description, error: errors?.[path], root, value, onChange, path, required, errors, idPrefix, fieldWording: props.fieldWording, choiceLabel: props.choiceLabel, }); } /** The props the per-kind renderer needs, computed once by {@link FieldNodeImpl}. */ interface RenderFieldArgs { readonly model: FieldModel; readonly nullable: boolean; readonly heading: string; readonly description: string | undefined; readonly error: string | undefined; readonly root: JsonSchema; readonly value: unknown; readonly onChange: (value: unknown) => void; readonly path: string; readonly required: boolean; readonly errors: SchemaFormErrors | undefined; readonly idPrefix: string; readonly fieldWording?: Readonly>; readonly choiceLabel?: string; } /** Render the field component matching the classified model kind. */ function renderField(args: RenderFieldArgs): ReactNode { const { model, nullable, heading, description, error, value, onChange, path, required } = args; switch (model.kind) { case 'json': return ( ); case 'const': return ( ); case 'enum': return ( ); case 'string': return ( ); case 'number': return ( ); case 'boolean': return ( ); case 'multiselect': return ( ); default: return renderCompositeField(args, model); } } /** The composite (recursing) field kinds: array, object, record, and union. */ function renderCompositeField( args: RenderFieldArgs, model: Extract, ): ReactNode { const { heading, description, error, root, value, onChange, path, errors, idPrefix } = args; const { fieldWording } = args; switch (model.kind) { case 'array': return ( ); case 'object': return ( ); case 'record': return ( ); case 'union': return ( ); } } export const FieldNode = memo(FieldNodeImpl); /** The label/description/error chrome every scalar leaf field shares. */ interface ScalarFieldChrome { readonly heading: string; readonly description: string | undefined; readonly error: string | undefined; } /** A `const`-pinned value: shown read-only, never editable. */ function ConstField({ heading, description, error, constValue, }: ScalarFieldChrome & { constValue: unknown }): ReactNode { return ( ); } /** A number/integer field; an empty input clears the value to `undefined`. */ function NumberField({ heading, description, error, integer, value, onChange, }: ScalarFieldChrome & { integer: boolean; value: unknown; onChange: (value: unknown) => void; }): ReactNode { return ( { const raw = event.target.value; onChange(raw === '' ? undefined : Number(raw)); }} /> ); } /** A boolean field rendered as a checkbox. */ function BooleanField({ heading, description, error, value, onChange, }: ScalarFieldChrome & { value: unknown; onChange: (value: unknown) => void }): ReactNode { return ( { onChange(checked); }} /> ); }