zod-form-kit
Version:
UI-agnostic form generation library based on Zod schemas with extensible adapter pattern
158 lines (157 loc) • 7.36 kB
JavaScript
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] });
}
} }));
}