UNPKG

zod-form-kit

Version:

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

110 lines (109 loc) 5.03 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { z } from 'zod'; import { useForm } from '@tanstack/react-form'; import { useLayoutEffect } from 'react'; import { FieldRenderer } from './FieldRenderer'; import { parseZodSchema } from '../utils/schema-parser'; import { registerUIAdapter as registerUIAdapterFn, setDefaultAdapter } from '../utils/plugin-registry'; // Helper function to extract default values from Zod schema const extractSchemaDefaults = (zodSchema) => { // Don't try to parse empty object - just extract defaults manually if (zodSchema instanceof z.ZodObject) { const defaults = {}; Object.entries(zodSchema.shape).forEach(([key, fieldSchema]) => { if (fieldSchema instanceof z.ZodDefault) { defaults[key] = fieldSchema._def.defaultValue(); } else if (fieldSchema instanceof z.ZodOptional) { // Optional fields get undefined defaults[key] = undefined; } else if (fieldSchema instanceof z.ZodString) { // Initialize string fields with empty string to avoid undefined defaults[key] = ''; } else if (fieldSchema instanceof z.ZodNumber) { // Initialize number fields with 0 defaults[key] = 0; } else if (fieldSchema instanceof z.ZodBoolean) { // Initialize boolean fields with false defaults[key] = false; } else if (fieldSchema instanceof z.ZodArray) { // Initialize arrays with empty array defaults[key] = []; } else if (fieldSchema instanceof z.ZodObject) { // Recursively handle nested objects defaults[key] = extractSchemaDefaults(fieldSchema); } else if (fieldSchema instanceof z.ZodEnum) { // Initialize enum with first option defaults[key] = fieldSchema.options[0]; } else if (fieldSchema instanceof z.ZodDiscriminatedUnion) { // Initialize discriminated union with first variant const firstVariant = fieldSchema.options[0]; if (firstVariant instanceof z.ZodObject) { const discriminatorKey = fieldSchema.discriminator; const discriminatorValue = firstVariant.shape[discriminatorKey].value; defaults[key] = { [discriminatorKey]: discriminatorValue, ...extractSchemaDefaults(firstVariant) }; } } else { // For other types, use undefined defaults[key] = undefined; } }); return defaults; } return {}; }; export function ZodForm({ schema, onSubmit, defaultValues = {}, className = '', registerUIAdapter }) { if (registerUIAdapter) { registerUIAdapterFn(registerUIAdapter); setDefaultAdapter(registerUIAdapter.name); } // Register UI adapter if provided and set as default useLayoutEffect(() => { if (registerUIAdapter) { registerUIAdapterFn(registerUIAdapter); setDefaultAdapter(registerUIAdapter.name); } }, [registerUIAdapter]); // Extract default values from schema and merge with provided defaults const schemaDefaults = extractSchemaDefaults(schema); const mergedDefaults = { ...schemaDefaults, ...defaultValues }; // Initialize the form using TanStack Form const form = useForm({ defaultValues: mergedDefaults, onSubmit: async ({ value }) => { // Validate with Zod before submitting try { const validatedData = schema.parse(value); await onSubmit(validatedData); } catch (error) { if (error instanceof z.ZodError) { // Don't throw the error - just log it and let the form handle it console.error('Validation error:', error); return; } throw error; } } }); const parsedSchema = parseZodSchema(schema); return (_jsxs("form", { onSubmit: (e) => { e.preventDefault(); e.stopPropagation(); form.handleSubmit(); }, className: `form-generator ${className}`, role: "form", children: [_jsx(FieldRenderer, { schema: parsedSchema, form: form, path: "", zodSchema: schema, isRoot: true }), _jsx(form.Subscribe, { selector: (state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting }), children: ({ canSubmit, isSubmitting }) => (_jsx("button", { type: "submit", disabled: !canSubmit || isSubmitting, className: "submit-button", children: isSubmitting ? 'Submitting...' : 'Submit' })) })] })); }