UNPKG

zod-form-kit

Version:

UI-agnostic form generation library based on Zod schemas with extensible adapter pattern

158 lines (157 loc) 7.36 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import React from 'react'; import { getRegisteredRenderer, getMatchingPatternRenderer } from '../utils/plugin-registry'; import { z } from 'zod'; // Helper function to get field schema from path const getFieldSchemaAtPath = (zodSchema, path) => { if (!path || !zodSchema) return zodSchema; const pathParts = path.split('.'); let currentSchema = zodSchema; for (const part of pathParts) { if (currentSchema instanceof z.ZodObject) { currentSchema = currentSchema.shape[part]; } else { return undefined; } } return currentSchema; }; export function FieldRenderer({ schema, form, path, label, zodSchema, isRoot = false }) { const fieldName = path || schema.name || ''; // Special handling for root-level objects // Instead of trying to render the root object as a single field, // render its properties directly to avoid TanStack Form path issues if (isRoot && schema.type === 'object' && schema.properties) { return (_jsx("div", { className: "root-object-fields", children: Object.entries(schema.properties).map(([key, propertySchema]) => { if (!propertySchema) { console.warn(`FieldRenderer: Schema for property '${key}' is undefined`); return null; } return (_jsx(FieldRenderer, { schema: propertySchema, form: form, path: key, label: key, zodSchema: zodSchema, isRoot: false }, key)); }) })); } // Get the specific Zod schema for this field for validation const fieldZodSchema = getFieldSchemaAtPath(zodSchema, fieldName); // STEP 1: Check for pattern-based custom renderers first let PatternComponent; if (fieldZodSchema) { PatternComponent = getMatchingPatternRenderer(fieldZodSchema, schema); } // STEP 2: If pattern renderer found, use it if (PatternComponent) { return (_jsx(form.Field, { name: fieldName, validators: { onChange: fieldZodSchema ? ({ value, fieldApi }) => { // Always validate if the field has been touched or if the form has been submitted const isTouched = fieldApi?.state?.meta?.isTouched; const hasErrors = fieldApi?.state?.meta?.errors?.length > 0; const isSubmitted = fieldApi?.state?.meta?.isSubmitted; const shouldValidate = isTouched || hasErrors || isSubmitted; if (!shouldValidate) { return undefined; } try { fieldZodSchema.parse(value); return undefined; } catch (error) { if (error instanceof z.ZodError) { return error.errors[0]?.message || 'Validation error'; } return 'Validation error'; } } : undefined }, children: (field) => { const patternProps = { value: field.state.value, onChange: (newValue) => field.handleChange(newValue), error: field.state.meta.errors.join(', ') || undefined, required: schema.required, label: label || path.split('.').pop() || '', name: fieldName, zodSchema: fieldZodSchema, parsedField: schema, form, path: fieldName }; return React.createElement(PatternComponent, patternProps); } })); } // STEP 3: Fall back to type-based renderers (existing behavior) const fieldType = schema.type; const FieldComponent = getRegisteredRenderer(fieldType); if (!FieldComponent) { return _jsxs("div", { children: ["Unsupported field type: ", schema.type] }); } return (_jsx(form.Field, { name: fieldName, validators: { onChange: fieldZodSchema ? ({ value, fieldApi }) => { // Always validate if the field has been touched or if the form has been submitted const isTouched = fieldApi?.state?.meta?.isTouched; const hasErrors = fieldApi?.state?.meta?.errors?.length > 0; const isSubmitted = fieldApi?.state?.meta?.isSubmitted; const shouldValidate = isTouched || hasErrors || isSubmitted; if (!shouldValidate) { return undefined; } try { fieldZodSchema.parse(value); return undefined; } catch (error) { if (error instanceof z.ZodError) { return error.errors[0]?.message || 'Validation error'; } return 'Validation error'; } } : undefined }, children: (field) => { const fieldProps = { value: field.state.value, onChange: (newValue) => field.handleChange(newValue), error: field.state.meta.errors.join(', ') || undefined, required: schema.required, label: label || path.split('.').pop() || '', name: fieldName }; switch (schema.type) { case 'string': return React.createElement(FieldComponent, { ...fieldProps, options: schema.options }); case 'number': return React.createElement(FieldComponent, { ...fieldProps, options: schema.options }); case 'boolean': return React.createElement(FieldComponent, fieldProps); case 'date': return React.createElement(FieldComponent, fieldProps); case 'enum': return React.createElement(FieldComponent, { ...fieldProps, values: schema.values }); case 'array': return React.createElement(FieldComponent, { ...fieldProps, itemSchema: schema.items, form, path: fieldName, options: schema.options }); case 'object': return React.createElement(FieldComponent, { ...fieldProps, properties: schema.properties || {}, form, path: fieldName, zodSchema }); case 'discriminatedUnion': return React.createElement(FieldComponent, { ...fieldProps, discriminator: schema.discriminator, variants: schema.variants, form, path: fieldName, zodSchema }); default: return _jsxs("div", { children: ["Unsupported field type: ", schema.type] }); } } })); }