Introduce core components and utilities for AutoForm, including form configuration, field types, dependency resolution, and schema handling using Zod. These additions provide a structured and dynamic form generation system with dependency management and validation integration.
176 lines
4.6 KiB
TypeScript
176 lines
4.6 KiB
TypeScript
import {
|
|
Accordion,
|
|
AccordionContent,
|
|
AccordionItem,
|
|
AccordionTrigger,
|
|
} from "@/components/ui/accordion";
|
|
import { FormField } from "@/components/ui/form";
|
|
import { type useForm, useFormContext } from "react-hook-form";
|
|
import * as z from "zod";
|
|
import { DEFAULT_ZOD_HANDLERS, INPUT_COMPONENTS } from "../config";
|
|
import resolveDependencies from "../dependencies";
|
|
import type { Dependency, FieldConfig, FieldConfigItem } from "../types";
|
|
import {
|
|
beautifyObjectName,
|
|
getBaseSchema,
|
|
getBaseType,
|
|
zodToHtmlInputProps,
|
|
} from "../utils";
|
|
import AutoFormArray from "./array";
|
|
|
|
function DefaultParent({ children }: { children: React.ReactNode }) {
|
|
return <>{children}</>;
|
|
}
|
|
|
|
export default function AutoFormObject<SchemaType extends z.ZodObject<any, any>>({
|
|
schema,
|
|
form,
|
|
fieldConfig,
|
|
path = [],
|
|
dependencies = [],
|
|
}: {
|
|
schema: SchemaType | z.ZodEffects<SchemaType>;
|
|
form: ReturnType<typeof useForm>;
|
|
fieldConfig?: FieldConfig<z.infer<SchemaType>>;
|
|
path?: string[];
|
|
dependencies?: Dependency<z.infer<SchemaType>>[];
|
|
}) {
|
|
const { watch } = useFormContext(); // Use useFormContext to access the watch function
|
|
|
|
if (!schema) {
|
|
return null;
|
|
}
|
|
const { shape } = getBaseSchema<SchemaType>(schema) || {};
|
|
|
|
if (!shape) {
|
|
return null;
|
|
}
|
|
|
|
const handleIfZodNumber = (item: z.ZodAny) => {
|
|
const isZodNumber = (item as any)._def.typeName === "ZodNumber";
|
|
const isInnerZodNumber = (item._def as any).innerType?._def?.typeName === "ZodNumber";
|
|
|
|
if (isZodNumber) {
|
|
(item as any)._def.coerce = true;
|
|
} else if (isInnerZodNumber) {
|
|
(item._def as any).innerType._def.coerce = true;
|
|
}
|
|
|
|
return item;
|
|
};
|
|
|
|
return (
|
|
<Accordion type="multiple" className="space-y-5 border-none">
|
|
{Object.keys(shape).map((name) => {
|
|
let item = shape[name] as z.ZodAny;
|
|
item = handleIfZodNumber(item) as z.ZodAny;
|
|
const zodBaseType = getBaseType(item);
|
|
const itemName = item._def.description ?? beautifyObjectName(name);
|
|
const key = [...path, name].join(".");
|
|
|
|
const {
|
|
isHidden,
|
|
isDisabled,
|
|
isRequired: isRequiredByDependency,
|
|
overrideOptions,
|
|
} = resolveDependencies(dependencies, name, watch);
|
|
if (isHidden) {
|
|
return null;
|
|
}
|
|
|
|
if (zodBaseType === "ZodObject") {
|
|
return (
|
|
<AccordionItem value={name} key={key} className="border-none">
|
|
<AccordionTrigger>{itemName}</AccordionTrigger>
|
|
<AccordionContent className="p-2">
|
|
<AutoFormObject
|
|
schema={item as unknown as z.ZodObject<any, any>}
|
|
form={form}
|
|
fieldConfig={
|
|
(fieldConfig?.[name] ?? {}) as FieldConfig<z.infer<typeof item>>
|
|
}
|
|
path={[...path, name]}
|
|
/>
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
);
|
|
}
|
|
if (zodBaseType === "ZodArray") {
|
|
return (
|
|
<AutoFormArray
|
|
key={key}
|
|
name={name}
|
|
item={item as unknown as z.ZodArray<any>}
|
|
form={form}
|
|
fieldConfig={fieldConfig?.[name] ?? {}}
|
|
path={[...path, name]}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const fieldConfigItem: FieldConfigItem = fieldConfig?.[name] ?? {};
|
|
const zodInputProps = zodToHtmlInputProps(item);
|
|
const isRequired =
|
|
isRequiredByDependency ||
|
|
zodInputProps.required ||
|
|
fieldConfigItem.inputProps?.required ||
|
|
false;
|
|
|
|
if (overrideOptions) {
|
|
item = z.enum(overrideOptions) as unknown as z.ZodAny;
|
|
}
|
|
|
|
return (
|
|
<FormField
|
|
control={form.control}
|
|
name={key}
|
|
key={key}
|
|
render={({ field }) => {
|
|
const inputType =
|
|
fieldConfigItem.fieldType ??
|
|
DEFAULT_ZOD_HANDLERS[zodBaseType] ??
|
|
"fallback";
|
|
|
|
const InputComponent =
|
|
typeof inputType === "function" ? inputType : INPUT_COMPONENTS[inputType];
|
|
|
|
const ParentElement = fieldConfigItem.renderParent ?? DefaultParent;
|
|
|
|
const defaultValue = fieldConfigItem.inputProps?.defaultValue;
|
|
const value = field.value ?? defaultValue ?? "";
|
|
|
|
const fieldProps = {
|
|
...zodToHtmlInputProps(item),
|
|
...field,
|
|
...fieldConfigItem.inputProps,
|
|
disabled: fieldConfigItem.inputProps?.disabled || isDisabled,
|
|
ref: undefined,
|
|
value: value,
|
|
};
|
|
|
|
if (InputComponent === undefined) {
|
|
return <></>;
|
|
}
|
|
|
|
return (
|
|
<ParentElement key={`${key}.parent`}>
|
|
<InputComponent
|
|
zodInputProps={zodInputProps}
|
|
field={field}
|
|
fieldConfigItem={fieldConfigItem}
|
|
label={itemName}
|
|
isRequired={isRequired}
|
|
zodItem={item}
|
|
fieldProps={fieldProps}
|
|
className={fieldProps.className}
|
|
/>
|
|
</ParentElement>
|
|
);
|
|
}}
|
|
/>
|
|
);
|
|
})}
|
|
</Accordion>
|
|
);
|
|
}
|