UNPKG

@form-kit/form-kit

Version:

Build dynamic forms based on zod schema or JSON

468 lines (451 loc) 17.2 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; 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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { FormKit: () => FormKit }); module.exports = __toCommonJS(index_exports); // src/form-kit.tsx var import_react2 = require("react"); var import_zod2 = require("@hookform/resolvers/zod"); var import_react_hook_form3 = require("react-hook-form"); // src/components/form.tsx var React2 = __toESM(require("react"), 1); var LabelPrimitive2 = require("@radix-ui/react-label"); var import_react_slot = require("@radix-ui/react-slot"); var import_react_hook_form = require("react-hook-form"); // utils/cn.ts var import_clsx = require("clsx"); var import_tailwind_merge = require("tailwind-merge"); function cn(...inputs) { return (0, import_tailwind_merge.twMerge)((0, import_clsx.clsx)(inputs)); } // src/components/label.tsx var React = require("react"); var LabelPrimitive = __toESM(require("@radix-ui/react-label"), 1); var import_jsx_runtime = require("react/jsx-runtime"); function Label({ className, ...props }) { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( LabelPrimitive.Root, { "data-slot": "label", className: cn( "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", className ), ...props } ); } // src/components/form.tsx var import_jsx_runtime2 = require("react/jsx-runtime"); var Form = import_react_hook_form.FormProvider; var FormFieldContext = React2.createContext({}); var FormField = ({ ...props }) => { return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(FormFieldContext.Provider, { value: { name: props.name }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_hook_form.Controller, { ...props }) }); }; var useFormField = () => { const fieldContext = React2.useContext(FormFieldContext); const itemContext = React2.useContext(FormItemContext); const { getFieldState } = (0, import_react_hook_form.useFormContext)(); const formState = (0, import_react_hook_form.useFormState)({ name: fieldContext.name }); const fieldState = getFieldState(fieldContext.name, formState); if (!fieldContext) { throw new Error("useFormField should be used within <FormField>"); } const { id } = itemContext; return { id, name: fieldContext.name, formItemId: `${id}-form-item`, formDescriptionId: `${id}-form-item-description`, formMessageId: `${id}-form-item-message`, ...fieldState }; }; var FormItemContext = React2.createContext({}); function FormItem({ className, ...props }) { const id = React2.useId(); return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(FormItemContext.Provider, { value: { id }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "data-slot": "form-item", className: cn("grid gap-2", className), ...props }) }); } function FormLabel({ className, ...props }) { const { error, formItemId } = useFormField(); return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( Label, { "data-slot": "form-label", "data-error": !!error, className: cn("data-[error=true]:text-destructive", className), htmlFor: formItemId, ...props } ); } function FormControl({ ...props }) { const { error, formItemId, formDescriptionId, formMessageId } = useFormField(); return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( import_react_slot.Slot, { "data-slot": "form-control", id: formItemId, "aria-describedby": !error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`, "aria-invalid": !!error, ...props } ); } function FormDescription({ className, ...props }) { const { formDescriptionId } = useFormField(); return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "p", { "data-slot": "form-description", id: formDescriptionId, className: cn("text-muted-foreground text-sm", className), ...props } ); } function FormMessage({ className, ...props }) { const { error, formMessageId } = useFormField(); const body = error ? String(error?.message ?? "") : props.children; if (!body) { return null; } return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "p", { "data-slot": "form-message", id: formMessageId, className: cn("text-destructive text-sm", className), ...props, children: body } ); } // src/form-field.tsx var import_react_hook_form2 = require("react-hook-form"); // src/fields/input.tsx var import_jsx_runtime3 = require("react/jsx-runtime"); var Input = (props) => { return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { type: "text", className: "h-10 bg-primary", ...props }); }; var input_default = Input; // src/fields/date.tsx var import_jsx_runtime4 = require("react/jsx-runtime"); var DateComponent = (props) => { return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("input", { type: "date", ...props }); }; var date_default = DateComponent; // src/fields/index.ts var BaseInputComponents = { text: input_default, date: date_default }; var fields_default = BaseInputComponents; // src/components/form-context.tsx var import_react = require("react"); var FormComponentsContext = (0, import_react.createContext)(void 0); var FormComponentsProvider = FormComponentsContext.Provider; var useFormComponents = () => { const ctx = (0, import_react.useContext)(FormComponentsContext); if (!ctx) throw new Error("FormComponentsContext not found"); return ctx; }; // src/field.tsx var import_jsx_runtime5 = require("react/jsx-runtime"); var Field = ({ inputType, ...props }) => { const components = useFormComponents(); const Component = { ...fields_default, ...components }[inputType]; if (!Component) return null; return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Component, { ...props }); }; var field_default = Field; // src/form-field.tsx var import_jsx_runtime6 = require("react/jsx-runtime"); var FormField2 = ({ metadata }) => { const { control } = (0, import_react_hook_form2.useForm)(); return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)( FormField, { control, name: metadata.name, render: ({ field }) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(FormItem, { children: [ metadata.label && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(FormLabel, { children: metadata.label }), /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(FormControl, { children: metadata.element ? metadata.element : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)( field_default, { inputType: metadata.type, placeholder: metadata.placeholder, ...metadata.fieldProps, ...field } ) }), metadata.description && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(FormDescription, { children: metadata.description }), /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(FormMessage, {}) ] }) } ); }; var form_field_default = FormField2; // src/components/button.tsx var React3 = require("react"); var import_react_slot2 = require("@radix-ui/react-slot"); var import_class_variance_authority = require("class-variance-authority"); var import_jsx_runtime7 = require("react/jsx-runtime"); var buttonVariants = (0, import_class_variance_authority.cva)( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline" }, size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", lg: "h-10 rounded-md px-6 has-[>svg]:px-4", icon: "size-9" } }, defaultVariants: { variant: "default", size: "default" } } ); function Button({ className, variant, size, asChild = false, ...props }) { const Comp = asChild ? import_react_slot2.Slot : "button"; return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)( Comp, { "data-slot": "button", className: cn(buttonVariants({ variant, size, className })), ...props } ); } // src/form-kit.tsx var import_zod3 = require("zod"); // utils/form.ts var import_zod = require("zod"); function generateGrid(inputs) { const GRID_WIDTH = 12; const result = []; let currentRow = []; let currentWidth = 0; const createPlaceholder = (size) => ({ name: `placeholder_${Math.random().toString(36).slice(2)}`, type: "hidden", size, label: "" }); for (const item of inputs) { const itemWidth = item.size || 12; if (itemWidth < 1 || itemWidth > 12) { throw new Error(`Invalid size ${itemWidth} for field ${item.name}`); } if (currentWidth + itemWidth > GRID_WIDTH || currentRow.length === 0 && itemWidth === GRID_WIDTH) { if (currentRow.length > 0) { while (currentWidth < GRID_WIDTH) { const remaining = GRID_WIDTH - currentWidth; currentRow.push(createPlaceholder(remaining)); currentWidth += remaining; } result.push(currentRow); } currentRow = []; currentWidth = 0; } currentRow.push({ ...item, size: itemWidth }); currentWidth += itemWidth; if (currentWidth === GRID_WIDTH) { result.push(currentRow); currentRow = []; currentWidth = 0; } } if (currentRow.length > 0) { while (currentWidth < GRID_WIDTH) { const remaining = GRID_WIDTH - currentWidth; currentRow.push(createPlaceholder(remaining)); currentWidth += remaining; } result.push(currentRow); } result.forEach((row, index) => { const rowWidth = row.reduce((sum, item) => sum + (item.size || 12), 0); if (rowWidth !== GRID_WIDTH) { throw new Error(`Row ${index} has invalid width: ${rowWidth}`); } }); return result; } function getDefaultValues(schema) { const shape = schema.shape; const defaultValues = {}; for (const item of Object.entries(shape)) { const [key, value] = item; const zodType = value; if ("_def" in zodType && "defaultValue" in zodType._def) { defaultValues[key] = zodType._def.defaultValue(); continue; } if (zodType instanceof import_zod.z.ZodString) { defaultValues[key] = ""; } else if (zodType instanceof import_zod.z.ZodNumber) { defaultValues[key] = 0; } else if (zodType instanceof import_zod.z.ZodBoolean) { defaultValues[key] = false; } else if (zodType instanceof import_zod.z.ZodArray) { defaultValues[key] = []; } else if (zodType instanceof import_zod.z.ZodObject) { defaultValues[key] = getDefaultValues(zodType); } else if (zodType instanceof import_zod.z.ZodOptional || zodType instanceof import_zod.z.ZodNullable) { defaultValues[key] = zodType instanceof import_zod.z.ZodOptional ? void 0 : null; } else { defaultValues[key] = void 0; } } return defaultValues; } function generateFields(schema, fieldTransformer) { const schemaShape = schema.shape; const defaultFields = Object.keys(schemaShape).map( (key) => ({ name: key, size: 12, type: "text", label: key }) ); if (!fieldTransformer) return defaultFields; const getTransformResult = (field, transformer) => { const { name } = field; const privateValues = { name }; if (typeof transformer === "function") { const transformResult = transformer(field); if (transformResult) { return { ...field, ...transformResult, ...privateValues }; } else return field; } else if (typeof transformer === "object") { const transformResult = transformer[name]; if (!transformResult) return field; return { ...field, ...typeof transformResult === "function" ? transformResult(field) : transformResult, ...privateValues }; } return field; }; return defaultFields.map((field) => { return getTransformResult(field, fieldTransformer); }); } // src/form-kit.tsx var import_jsx_runtime8 = require("react/jsx-runtime"); var FormKit = (props) => { const { schema, initialValues, fields = [], onSubmit, onCancel, fieldTransformer, components = {} } = props; const initialFormValues = (0, import_react2.useMemo)(() => { if (initialValues) return initialValues; if (!schema) return {}; return getDefaultValues(schema); }, [initialValues, schema]); const form = (0, import_react_hook_form3.useForm)({ resolver: (0, import_zod2.zodResolver)(schema), values: initialFormValues }); const isSubmitting = form.formState.isSubmitting; const formFields = (0, import_react2.useMemo)(() => { if (fields.length > 0) return fields; if (!schema) return []; return generateFields(schema, fieldTransformer); }, [fields, schema, fieldTransformer]); const rows = (0, import_react2.useMemo)(() => generateGrid(formFields), [formFields]); const handleCancel = () => { form.reset(); onCancel?.(); }; const handleSubmit = async (data) => { await onSubmit?.(data); }; return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(FormComponentsProvider, { value: components, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Form, { ...form, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("form", { onSubmit: form.handleSubmit(handleSubmit), className: "flex flex-col gap-6", children: [ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "flex flex-col gap-4", children: rows.map((row, index) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "grid grid-cols-12 gap-4", children: row.map((col) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)( "div", { className: "w-full", style: { gridColumn: `span ${col.size} / span ${col.size}` }, children: col.type !== "hidden" && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(form_field_default, { metadata: col }) }, col.name )) }, index)) }), /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", children: [ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Button, { type: "button", variant: "outline", onClick: handleCancel, disabled: isSubmitting, children: "Cancel" }), /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Button, { type: "submit", disabled: isSubmitting, children: isSubmitting ? "Saving..." : "Save Changes" }) ] }) ] }) }) }); }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { FormKit }); //# sourceMappingURL=index.cjs.map