UNPKG

el-form-react-components

Version:

AutoForm and UI components for React - generate forms instantly from schemas (Zod, Yup, Valibot). Includes Tailwind CSS styling and customizable field components.

745 lines (742 loc) 26.2 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { AutoForm: () => AutoForm, SelectField: () => SelectField, TextField: () => TextField, TextareaField: () => TextareaField, createField: () => createField }); module.exports = __toCommonJS(src_exports); // src/AutoForm.tsx var import_el_form_react_hooks = require("el-form-react-hooks"); var import_zod = require("zod"); var import_jsx_runtime = require("react/jsx-runtime"); var DefaultErrorComponent = ({ errors, touched }) => { const errorEntries = Object.entries(errors).filter( ([field]) => touched[field] ); if (errorEntries.length === 0) return null; return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "el-form-error-summary", children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { children: "\u26A0\uFE0F Please fix the following errors:" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { children: errorEntries.map(([field, error]) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: "#ef4444", marginRight: "0.5rem" }, children: "\u2022" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { textTransform: "capitalize" }, children: [ field, ":" ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { marginLeft: "0.25rem" }, children: String(error) }) ] }, field)) }) ] }); }; var DefaultField = ({ name, label, type = "text", placeholder, value, onChange, onBlur, error, touched, options, className, inputClassName, labelClassName, errorClassName }) => { const fieldId = `field-${name}`; if (type === "checkbox") { return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: className || "flex items-center gap-x-2", children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "input", { id: fieldId, name, type: "checkbox", checked: !!value, onChange, onBlur, className: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "label", { htmlFor: fieldId, className: labelClassName || "text-sm font-medium text-gray-900", children: label } ) ] }); } const inputClasses = inputClassName || ` w-full px-3 py-2 border rounded-md text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${touched && error ? "border-red-500 focus:ring-red-500 focus:border-red-500" : "border-gray-300"} `.trim().replace(/\s+/g, " "); return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: className || "space-y-1", children: [ label && /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "label", { htmlFor: fieldId, className: labelClassName || "block text-sm font-medium text-gray-700", children: label } ), type === "textarea" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "textarea", { id: fieldId, name, value: value || "", onChange, onBlur, placeholder, className: `${inputClasses} resize-none`, rows: 4 } ) : type === "select" && options ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)( "select", { id: fieldId, name, value: value || "", onChange, onBlur, className: inputClasses, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "", children: placeholder || "Select an option" }), options.map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: option.value, children: option.label }, option.value)) ] } ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "input", { id: fieldId, name, type, value: value || "", onChange, onBlur, placeholder, className: inputClasses } ), touched && error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: errorClassName || "text-red-500 text-xs mt-1", children: error }) ] }); }; var ArrayField = ({ fieldConfig, value = [], path, onAddItem, onRemoveItem, onValueChange, register, formState }) => { const arrayValue = Array.isArray(value) ? value : []; const createEmptyItem = () => { if (!fieldConfig.fields) return {}; if (fieldConfig.fields.length === 1 && fieldConfig.fields[0].name === "value") { const fieldType = fieldConfig.fields[0].type; if (fieldType === "number") { return 0; } else if (fieldType === "checkbox") { return false; } else { return ""; } } const emptyItem = {}; fieldConfig.fields.forEach((field) => { if (field.type === "array") { emptyItem[field.name] = []; } else if (field.type === "number") { emptyItem[field.name] = 0; } else { emptyItem[field.name] = ""; } }); return emptyItem; }; const handleAddItem = () => { onAddItem(path, createEmptyItem()); }; const handleRemoveItem = (index) => { onRemoveItem(path, index); }; const renderNestedField = (nestedFieldConfig, itemIndex, itemPath) => { if (nestedFieldConfig.name === "value") { const fieldValue2 = arrayValue[itemIndex] || ""; return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "space-y-1", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "input", { type: nestedFieldConfig.type || "text", value: fieldValue2, onChange: (e) => { const newValue = nestedFieldConfig.type === "number" ? e.target.value ? Number(e.target.value) : 0 : e.target.value; onValueChange(itemPath, newValue); }, placeholder: nestedFieldConfig.placeholder || "Enter value", className: "w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" } ) }, itemPath); } const fieldPath = `${itemPath}.${nestedFieldConfig.name}`; const fieldValue = arrayValue[itemIndex]?.[nestedFieldConfig.name] || ""; if (nestedFieldConfig.type === "array") { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( ArrayField, { fieldConfig: nestedFieldConfig, value: fieldValue, path: fieldPath, onAddItem, onRemoveItem, onValueChange, register, formState }, fieldPath ); } return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-1", children: [ nestedFieldConfig.label && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "block text-sm font-medium text-gray-700", children: nestedFieldConfig.label }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "input", { type: nestedFieldConfig.type || "text", value: fieldValue, onChange: (e) => { const newValue = nestedFieldConfig.type === "number" ? e.target.value ? Number(e.target.value) : 0 : e.target.value; onValueChange(fieldPath, newValue); }, placeholder: nestedFieldConfig.placeholder, className: "w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" } ) ] }, fieldPath); }; return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-3", children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between", children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "block text-sm font-medium text-gray-700", children: fieldConfig.label || fieldConfig.name }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)( "button", { type: "button", onClick: handleAddItem, className: "px-3 py-1 bg-green-600 text-white text-xs rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500", children: [ "+ Add ", fieldConfig.label || fieldConfig.name ] } ) ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "space-y-4", children: arrayValue.map((_, index) => { const itemPath = `${path}[${index}]`; return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)( "div", { className: "p-4 border border-gray-200 rounded-lg bg-gray-50", children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex justify-between items-center mb-3", children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("h4", { className: "text-sm font-medium text-gray-700", children: [ fieldConfig.label || fieldConfig.name, " #", index + 1 ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { type: "button", onClick: () => handleRemoveItem(index), className: "px-2 py-1 bg-red-600 text-white text-xs rounded hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500", children: "Remove" } ) ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3", children: fieldConfig.fields?.map( (nestedField) => renderNestedField(nestedField, index, itemPath) ) }) ] }, index ); }) }), arrayValue.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "text-gray-500 text-sm italic text-center py-4", children: [ "No ", fieldConfig.label?.toLowerCase() || fieldConfig.name, ' added yet. Click "Add" to create one.' ] }) ] }); }; function generateFieldsFromSchema(schema) { if (!(schema instanceof import_zod.z.ZodObject)) { return []; } const shape = schema.shape; const fields = []; for (const key in shape) { if (Object.prototype.hasOwnProperty.call(shape, key)) { const zodType = shape[key]; const typeName = zodType._def.typeName; const fieldConfig = { name: key, label: key.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase()), type: "text" // Default to text }; if (typeName === "ZodString") { const checks = zodType._def.checks || []; if (checks.some((c) => c.kind === "email")) { fieldConfig.type = "email"; } else if (checks.some((c) => c.kind === "url")) { fieldConfig.type = "url"; } } else if (typeName === "ZodNumber") { fieldConfig.type = "number"; } else if (typeName === "ZodBoolean") { fieldConfig.type = "checkbox"; } else if (typeName === "ZodEnum") { fieldConfig.type = "select"; fieldConfig.options = zodType._def.values.map((v) => ({ value: v, label: v })); } else if (typeName === "ZodDate") { fieldConfig.type = "date"; } else if (typeName === "ZodArray") { fieldConfig.type = "array"; const arrayElementType = zodType._def.type; if (arrayElementType instanceof import_zod.z.ZodObject) { fieldConfig.fields = generateFieldsFromSchema(arrayElementType); } else { const elementTypeName = arrayElementType._def.typeName; let elementType = "text"; if (elementTypeName === "ZodString") { elementType = "text"; } else if (elementTypeName === "ZodNumber") { elementType = "number"; } else if (elementTypeName === "ZodBoolean") { elementType = "checkbox"; } fieldConfig.fields = [ { name: "value", type: elementType, label: "Value" } ]; } } fields.push(fieldConfig); } } return fields; } function mergeFields(autoFields, manualFields) { const manualFieldsMap = new Map( manualFields.map((field) => [field.name, field]) ); const mergedFields = autoFields.map((autoField) => { const manualField = manualFieldsMap.get(autoField.name); if (manualField) { return { ...autoField, ...manualField }; } return autoField; }); manualFields.forEach((manualField) => { if (!autoFields.some((autoField) => autoField.name === manualField.name)) { mergedFields.push(manualField); } }); return mergedFields; } function AutoForm({ schema, fields, initialValues = {}, layout = "flex", columns = 12, onSubmit, onError, children, customErrorComponent, componentMap, validators, fieldValidators, validateOn = "onChange", submitButtonProps, resetButtonProps }) { const formValidators = validators || { [validateOn === "manual" ? "onSubmit" : validateOn]: schema }; const formApi = (0, import_el_form_react_hooks.useForm)({ validators: formValidators, fieldValidators, defaultValues: initialValues, validateOn }); const { register, handleSubmit, formState, reset, setValue, addArrayItem, removeArrayItem } = formApi; const autoGeneratedFields = generateFieldsFromSchema(schema); const fieldsToRender = fields ? mergeFields(autoGeneratedFields, fields) : autoGeneratedFields; const ErrorComponent = customErrorComponent || DefaultErrorComponent; const renderField = (fieldConfig) => { const fieldName = fieldConfig.name; const getColSpanClass = (colSpan) => { const spanMap = { 1: "col-span-1", 2: "col-span-2", 3: "col-span-3", 4: "col-span-4", 5: "col-span-5", 6: "col-span-6", 7: "col-span-7", 8: "col-span-8", 9: "col-span-9", 10: "col-span-10", 11: "col-span-11", 12: "col-span-12" }; return spanMap[colSpan || 1]; }; const getFlexClass = (colSpan) => { const flexMap = { 1: "w-1/12", 2: "w-2/12", 3: "w-3/12", 4: "w-4/12", 5: "w-5/12", 6: "w-6/12", 7: "w-7/12", 8: "w-8/12", 9: "w-9/12", 10: "w-10/12", 11: "w-11/12", 12: "w-full" }; return flexMap[colSpan || 12]; }; const fieldContainerClasses = layout === "grid" ? getColSpanClass(fieldConfig.colSpan) : `flex-none ${getFlexClass(fieldConfig.colSpan)}`; if (fieldConfig.type === "array") { const fieldProps2 = register(String(fieldName)); const fieldValue2 = "value" in fieldProps2 ? fieldProps2.value : []; const arrayValue = Array.isArray(fieldValue2) ? fieldValue2 : []; return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: fieldContainerClasses, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)( ArrayField, { fieldConfig, value: arrayValue, path: fieldConfig.name, onAddItem: addArrayItem, onRemoveItem: removeArrayItem, onValueChange: setValue, register, formState } ) }, fieldConfig.name); } const fieldProps = register(String(fieldName)); const error = formState.errors[fieldName]; const touched = formState.touched[fieldName]; const fieldValue = "checked" in fieldProps ? fieldProps.checked : "value" in fieldProps ? fieldProps.value : void 0; const FieldComponent = fieldConfig.component || fieldConfig.type && componentMap?.[fieldConfig.type] || DefaultField; return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: fieldContainerClasses, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)( FieldComponent, { name: fieldConfig.name, label: fieldConfig.label || fieldConfig.name, type: fieldConfig.type, placeholder: fieldConfig.placeholder, value: fieldValue, onChange: fieldProps.onChange, onBlur: fieldProps.onBlur, error, touched, options: fieldConfig.options, className: fieldConfig.className, inputClassName: fieldConfig.inputClassName, labelClassName: fieldConfig.labelClassName, errorClassName: fieldConfig.errorClassName } ) }, fieldConfig.name); }; const getGridClass = (cols) => { const gridMap = { 1: "grid-cols-1", 2: "grid-cols-2", 3: "grid-cols-3", 4: "grid-cols-4", 5: "grid-cols-5", 6: "grid-cols-6", 7: "grid-cols-7", 8: "grid-cols-8", 9: "grid-cols-9", 10: "grid-cols-10", 11: "grid-cols-11", 12: "grid-cols-12" }; return gridMap[cols]; }; const containerClasses = layout === "grid" ? `grid ${getGridClass(columns)} gap-4` : `flex flex-wrap gap-4`; const defaultForm = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)( "form", { onSubmit: handleSubmit( (data) => onSubmit(data), onError || ((errors) => console.error("Form validation errors:", errors)) ), className: "w-full", children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)( ErrorComponent, { errors: formState.errors, touched: formState.touched } ), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: containerClasses, children: [ fieldsToRender.map(renderField), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)( "div", { className: ` flex gap-3 mt-6 ${layout === "grid" ? "col-span-full" : "w-full"} `.trim().replace(/\s+/g, " "), children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { type: "submit", disabled: formState.isSubmitting, className: "el-form-submit-button px-5 py-2.5 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-60 disabled:cursor-not-allowed transition-colors duration-200", ...submitButtonProps, children: formState.isSubmitting ? "Submitting..." : "Submit" } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { type: "button", onClick: () => reset(), className: "px-5 py-2.5 bg-gray-600 text-white rounded-md text-sm font-medium hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors duration-200", ...resetButtonProps, children: "Reset" } ) ] } ) ] }) ] } ); if (children) { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)( "form", { onSubmit: handleSubmit( (data) => onSubmit(data), onError || ((errors) => console.error("Form validation errors:", errors)) ), className: "w-full", children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)( ErrorComponent, { errors: formState.errors, touched: formState.touched } ), children(formApi), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: containerClasses, children: fieldsToRender.map(renderField) }) ] } ) }); } return defaultForm; } // src/FieldComponents.tsx var import_el_form_react_hooks2 = require("el-form-react-hooks"); var import_jsx_runtime2 = require("react/jsx-runtime"); function createField(name) { return { name, // Type-safe value getter getValue: (form) => form.formState.values[name], // Type-safe error getter getError: (form) => form.formState.errors[name], // Type-safe touched getter getTouched: (form) => form.formState.touched[name], // Register function register: (form) => form.register(String(name)) }; } function TextField({ name, label, placeholder, className = "", type = "text", ...props }) { const form = (0, import_el_form_react_hooks2.useFormContext)(); const field = createField(name); const error = field.getError(form.form); const touched = field.getTouched(form.form); const registration = field.register(form.form); const inputClasses = ` w-full px-3 py-2 border rounded-md text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${touched && error ? "border-red-500 focus:ring-red-500 focus:border-red-500" : "border-gray-300"} ${className} `.trim().replace(/\s+/g, " "); return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "space-y-1", children: [ label && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "label", { htmlFor: String(name), className: "block text-sm font-medium text-gray-700", children: label } ), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "input", { ...registration, ...props, id: String(name), type, placeholder, className: inputClasses } ), touched && error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "text-red-500 text-xs mt-1", children: error }) ] }); } function TextareaField({ name, label, placeholder, className = "", rows = 4, ...props }) { const form = (0, import_el_form_react_hooks2.useFormContext)(); const field = createField(name); const error = field.getError(form.form); const touched = field.getTouched(form.form); const registration = field.register(form.form); const textareaClasses = ` w-full px-3 py-2 border rounded-md text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none ${touched && error ? "border-red-500 focus:ring-red-500 focus:border-red-500" : "border-gray-300"} ${className} `.trim().replace(/\s+/g, " "); return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "space-y-1", children: [ label && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "label", { htmlFor: String(name), className: "block text-sm font-medium text-gray-700", children: label } ), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "textarea", { ...registration, ...props, id: String(name), placeholder, rows, className: textareaClasses } ), touched && error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "text-red-500 text-xs mt-1", children: error }) ] }); } function SelectField({ name, label, placeholder, className = "", options = [], ...props }) { const form = (0, import_el_form_react_hooks2.useFormContext)(); const field = createField(name); const error = field.getError(form.form); const touched = field.getTouched(form.form); const registration = field.register(form.form); const selectClasses = ` w-full px-3 py-2 border rounded-md text-sm text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${touched && error ? "border-red-500 focus:ring-red-500 focus:border-red-500" : "border-gray-300"} ${className} `.trim().replace(/\s+/g, " "); return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "space-y-1", children: [ label && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "label", { htmlFor: String(name), className: "block text-sm font-medium text-gray-700", children: label } ), /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)( "select", { ...registration, ...props, id: String(name), className: selectClasses, children: [ placeholder && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: "", disabled: true, children: placeholder }), options.map((option) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: option.value, children: option.label }, option.value)) ] } ), touched && error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "text-red-500 text-xs mt-1", children: error }) ] }); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { AutoForm, SelectField, TextField, TextareaField, createField }); //# sourceMappingURL=index.js.map