@form-kit/form-kit
Version:
Build dynamic forms based on zod schema or JSON
468 lines (451 loc) • 17.2 kB
JavaScript
;
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