diff --git a/src/components/auto-form/common/label.tsx b/src/components/auto-form/common/label.tsx
new file mode 100644
index 0000000..df4cdec
--- /dev/null
+++ b/src/components/auto-form/common/label.tsx
@@ -0,0 +1,23 @@
+import { FormLabel } from "@/components/ui/form";
+import { cn } from "@/lib/utils";
+
+function AutoFormLabel({
+ label,
+ isRequired,
+ className,
+}: {
+ label: string;
+ isRequired: boolean;
+ className?: string;
+}) {
+ return (
+ <>
+
+ {label}
+ {isRequired && *}
+
+ >
+ );
+}
+
+export default AutoFormLabel;
diff --git a/src/components/auto-form/common/tooltip.tsx b/src/components/auto-form/common/tooltip.tsx
new file mode 100644
index 0000000..8ffb047
--- /dev/null
+++ b/src/components/auto-form/common/tooltip.tsx
@@ -0,0 +1,13 @@
+function AutoFormTooltip({ fieldConfigItem }: { fieldConfigItem: any }) {
+ return (
+ <>
+ {fieldConfigItem?.description && (
+
+ {fieldConfigItem.description}
+
+ )}
+ >
+ );
+}
+
+export default AutoFormTooltip;
diff --git a/src/components/auto-form/config.ts b/src/components/auto-form/config.ts
new file mode 100644
index 0000000..b98361a
--- /dev/null
+++ b/src/components/auto-form/config.ts
@@ -0,0 +1,35 @@
+import AutoFormCheckbox from "./fields/checkbox";
+import AutoFormDate from "./fields/date";
+import AutoFormEnum from "./fields/enum";
+import AutoFormFile from "./fields/file";
+import AutoFormInput from "./fields/input";
+import AutoFormNumber from "./fields/number";
+import AutoFormRadioGroup from "./fields/radio-group";
+import AutoFormSwitch from "./fields/switch";
+import AutoFormTextarea from "./fields/textarea";
+
+export const INPUT_COMPONENTS = {
+ checkbox: AutoFormCheckbox,
+ date: AutoFormDate,
+ select: AutoFormEnum,
+ radio: AutoFormRadioGroup,
+ switch: AutoFormSwitch,
+ textarea: AutoFormTextarea,
+ number: AutoFormNumber,
+ file: AutoFormFile,
+ fallback: AutoFormInput,
+};
+
+/**
+ * Define handlers for specific Zod types.
+ * You can expand this object to support more types.
+ */
+export const DEFAULT_ZOD_HANDLERS: {
+ [key: string]: keyof typeof INPUT_COMPONENTS;
+} = {
+ ZodBoolean: "checkbox",
+ ZodDate: "date",
+ ZodEnum: "select",
+ ZodNativeEnum: "select",
+ ZodNumber: "number",
+};
diff --git a/src/components/auto-form/dependencies.ts b/src/components/auto-form/dependencies.ts
new file mode 100644
index 0000000..0e28c09
--- /dev/null
+++ b/src/components/auto-form/dependencies.ts
@@ -0,0 +1,57 @@
+import type { FieldValues, UseFormWatch } from "react-hook-form";
+import type * as z from "zod";
+import { type Dependency, DependencyType, type EnumValues } from "./types";
+
+export default function resolveDependencies<
+ SchemaType extends z.infer>,
+>(
+ dependencies: Dependency[],
+ currentFieldName: keyof SchemaType,
+ watch: UseFormWatch,
+) {
+ let isDisabled = false;
+ let isHidden = false;
+ let isRequired = false;
+ let overrideOptions: EnumValues | undefined;
+
+ const currentFieldValue = watch(currentFieldName as string);
+
+ const currentFieldDependencies = dependencies.filter(
+ (dependency) => dependency.targetField === currentFieldName,
+ );
+ for (const dependency of currentFieldDependencies) {
+ const watchedValue = watch(dependency.sourceField as string);
+
+ const conditionMet = dependency.when(watchedValue, currentFieldValue);
+
+ switch (dependency.type) {
+ case DependencyType.DISABLES:
+ if (conditionMet) {
+ isDisabled = true;
+ }
+ break;
+ case DependencyType.REQUIRES:
+ if (conditionMet) {
+ isRequired = true;
+ }
+ break;
+ case DependencyType.HIDES:
+ if (conditionMet) {
+ isHidden = true;
+ }
+ break;
+ case DependencyType.SETS_OPTIONS:
+ if (conditionMet) {
+ overrideOptions = dependency.options;
+ }
+ break;
+ }
+ }
+
+ return {
+ isDisabled,
+ isHidden,
+ isRequired,
+ overrideOptions,
+ };
+}
diff --git a/src/components/auto-form/fields/array.tsx b/src/components/auto-form/fields/array.tsx
new file mode 100644
index 0000000..5391dae
--- /dev/null
+++ b/src/components/auto-form/fields/array.tsx
@@ -0,0 +1,91 @@
+import {
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from "@/components/ui/accordion";
+import { Button } from "@/components/ui/button";
+import { Separator } from "@/components/ui/separator";
+import { Plus, Trash } from "lucide-react";
+import { useFieldArray, type useForm } from "react-hook-form";
+import * as z from "zod";
+import { beautifyObjectName } from "../utils";
+import AutoFormObject from "./object";
+
+function isZodArray(item: z.ZodArray | z.ZodDefault): item is z.ZodArray {
+ return item instanceof z.ZodArray;
+}
+
+function isZodDefault(
+ item: z.ZodArray | z.ZodDefault,
+): item is z.ZodDefault {
+ return item instanceof z.ZodDefault;
+}
+
+export default function AutoFormArray({
+ name,
+ item,
+ form,
+ path = [],
+ fieldConfig,
+}: {
+ name: string;
+ item: z.ZodArray | z.ZodDefault;
+ form: ReturnType;
+ path?: string[];
+ fieldConfig?: any;
+}) {
+ const { fields, append, remove } = useFieldArray({
+ control: form.control,
+ name,
+ });
+ const title = item._def.description ?? beautifyObjectName(name);
+
+ const itemDefType = isZodArray(item)
+ ? item._def.type
+ : isZodDefault(item)
+ ? item._def.innerType._def.type
+ : null;
+
+ return (
+
+ {title}
+
+ {fields.map((_field, index) => {
+ const key = _field.id;
+ return (
+
+
}
+ form={form}
+ fieldConfig={fieldConfig}
+ path={[...path, index.toString()]}
+ />
+
+
+
+
+
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/src/components/auto-form/fields/checkbox.tsx b/src/components/auto-form/fields/checkbox.tsx
new file mode 100644
index 0000000..3f20a77
--- /dev/null
+++ b/src/components/auto-form/fields/checkbox.tsx
@@ -0,0 +1,34 @@
+import { Checkbox } from "@/components/ui/checkbox";
+import { FormControl, FormItem } from "@/components/ui/form";
+import AutoFormLabel from "../common/label";
+import AutoFormTooltip from "../common/tooltip";
+import type { AutoFormInputComponentProps } from "../types";
+
+export default function AutoFormCheckbox({
+ label,
+ isRequired,
+ field,
+ fieldConfigItem,
+ fieldProps,
+}: AutoFormInputComponentProps) {
+ return (
+
+ );
+}
diff --git a/src/components/auto-form/fields/date.tsx b/src/components/auto-form/fields/date.tsx
new file mode 100644
index 0000000..b88c92d
--- /dev/null
+++ b/src/components/auto-form/fields/date.tsx
@@ -0,0 +1,25 @@
+import { DatePicker } from "@/components/ui/date-picker";
+import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
+import AutoFormLabel from "../common/label";
+import AutoFormTooltip from "../common/tooltip";
+import type { AutoFormInputComponentProps } from "../types";
+
+export default function AutoFormDate({
+ label,
+ isRequired,
+ field,
+ fieldConfigItem,
+ fieldProps,
+}: AutoFormInputComponentProps) {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/auto-form/fields/enum.tsx b/src/components/auto-form/fields/enum.tsx
new file mode 100644
index 0000000..24702bf
--- /dev/null
+++ b/src/components/auto-form/fields/enum.tsx
@@ -0,0 +1,59 @@
+import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import type * as z from "zod";
+import AutoFormLabel from "../common/label";
+import AutoFormTooltip from "../common/tooltip";
+import type { AutoFormInputComponentProps } from "../types";
+import { getBaseSchema } from "../utils";
+
+export default function AutoFormEnum({
+ label,
+ isRequired,
+ field,
+ fieldConfigItem,
+ zodItem,
+ fieldProps,
+}: AutoFormInputComponentProps) {
+ const baseValues = (getBaseSchema(zodItem) as unknown as z.ZodEnum)._def.values;
+
+ let values: [string, string][] = [];
+ if (!Array.isArray(baseValues)) {
+ values = Object.entries(baseValues);
+ } else {
+ values = baseValues.map((value) => [value, value]);
+ }
+
+ function findItem(value: any) {
+ return values.find((item) => item[0] === value);
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/auto-form/fields/file.tsx b/src/components/auto-form/fields/file.tsx
new file mode 100644
index 0000000..d56020e
--- /dev/null
+++ b/src/components/auto-form/fields/file.tsx
@@ -0,0 +1,64 @@
+import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { Trash2 } from "lucide-react";
+import { type ChangeEvent, useState } from "react";
+import AutoFormLabel from "../common/label";
+import AutoFormTooltip from "../common/tooltip";
+import type { AutoFormInputComponentProps } from "../types";
+export default function AutoFormFile({
+ label,
+ isRequired,
+ fieldConfigItem,
+ fieldProps,
+ field,
+}: AutoFormInputComponentProps) {
+ const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = fieldProps;
+ const showLabel = _showLabel === undefined ? true : _showLabel;
+ const [file, setFile] = useState(null);
+ const [fileName, setFileName] = useState(null);
+ const handleFileChange = (e: ChangeEvent) => {
+ const file = e.target.files?.[0];
+
+ if (file) {
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ setFile(reader.result as string);
+ setFileName(file.name);
+ field.onChange(reader.result as string);
+ };
+ reader.readAsDataURL(file);
+ }
+ };
+
+ const handleRemoveClick = () => {
+ setFile(null);
+ };
+
+ return (
+
+ {showLabel && (
+
+ )}
+ {!file && (
+
+
+
+ )}
+ {file && (
+
+ )}
+
+
+
+ );
+}
diff --git a/src/components/auto-form/fields/input.tsx b/src/components/auto-form/fields/input.tsx
new file mode 100644
index 0000000..0ed0c24
--- /dev/null
+++ b/src/components/auto-form/fields/input.tsx
@@ -0,0 +1,34 @@
+import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import AutoFormLabel from "../common/label";
+import AutoFormTooltip from "../common/tooltip";
+import type { AutoFormInputComponentProps } from "../types";
+
+export default function AutoFormInput({
+ label,
+ isRequired,
+ fieldConfigItem,
+ fieldProps,
+}: AutoFormInputComponentProps) {
+ const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = fieldProps;
+ const showLabel = _showLabel === undefined ? true : _showLabel;
+ const type = fieldProps.type || "text";
+
+ return (
+
+ );
+}
diff --git a/src/components/auto-form/fields/number.tsx b/src/components/auto-form/fields/number.tsx
new file mode 100644
index 0000000..cb1d86b
--- /dev/null
+++ b/src/components/auto-form/fields/number.tsx
@@ -0,0 +1,28 @@
+import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import AutoFormLabel from "../common/label";
+import AutoFormTooltip from "../common/tooltip";
+import type { AutoFormInputComponentProps } from "../types";
+
+export default function AutoFormNumber({
+ label,
+ isRequired,
+ fieldConfigItem,
+ fieldProps,
+}: AutoFormInputComponentProps) {
+ const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = fieldProps;
+ const showLabel = _showLabel === undefined ? true : _showLabel;
+
+ return (
+
+ {showLabel && (
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/auto-form/fields/object.tsx b/src/components/auto-form/fields/object.tsx
new file mode 100644
index 0000000..bf6f3e3
--- /dev/null
+++ b/src/components/auto-form/fields/object.tsx
@@ -0,0 +1,175 @@
+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>({
+ schema,
+ form,
+ fieldConfig,
+ path = [],
+ dependencies = [],
+}: {
+ schema: SchemaType | z.ZodEffects;
+ form: ReturnType;
+ fieldConfig?: FieldConfig>;
+ path?: string[];
+ dependencies?: Dependency>[];
+}) {
+ const { watch } = useFormContext(); // Use useFormContext to access the watch function
+
+ if (!schema) {
+ return null;
+ }
+ const { shape } = getBaseSchema(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 (
+
+ {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 (
+
+ {itemName}
+
+ }
+ form={form}
+ fieldConfig={
+ (fieldConfig?.[name] ?? {}) as FieldConfig>
+ }
+ path={[...path, name]}
+ />
+
+
+ );
+ }
+ if (zodBaseType === "ZodArray") {
+ return (
+ }
+ 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 (
+ {
+ 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 (
+
+
+
+ );
+ }}
+ />
+ );
+ })}
+
+ );
+}
diff --git a/src/components/auto-form/fields/radio-group.tsx b/src/components/auto-form/fields/radio-group.tsx
new file mode 100644
index 0000000..4d4ee05
--- /dev/null
+++ b/src/components/auto-form/fields/radio-group.tsx
@@ -0,0 +1,51 @@
+import { FormControl, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
+import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
+import type * as z from "zod";
+import AutoFormLabel from "../common/label";
+import AutoFormTooltip from "../common/tooltip";
+import type { AutoFormInputComponentProps } from "../types";
+import { getBaseSchema } from "../utils";
+
+export default function AutoFormRadioGroup({
+ label,
+ isRequired,
+ field,
+ zodItem,
+ fieldProps,
+ fieldConfigItem,
+}: AutoFormInputComponentProps) {
+ const baseValues = (getBaseSchema(zodItem) as unknown as z.ZodEnum)._def.values;
+
+ let values: string[] = [];
+ if (!Array.isArray(baseValues)) {
+ values = Object.entries(baseValues).map((item) => item[0]);
+ } else {
+ values = baseValues;
+ }
+
+ return (
+
+
+
+
+
+ {values?.map((value: any) => (
+
+
+
+
+ {value}
+
+ ))}
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/auto-form/fields/switch.tsx b/src/components/auto-form/fields/switch.tsx
new file mode 100644
index 0000000..5a10dfc
--- /dev/null
+++ b/src/components/auto-form/fields/switch.tsx
@@ -0,0 +1,34 @@
+import { FormControl, FormItem } from "@/components/ui/form";
+import { Switch } from "@/components/ui/switch";
+import AutoFormLabel from "../common/label";
+import AutoFormTooltip from "../common/tooltip";
+import type { AutoFormInputComponentProps } from "../types";
+
+export default function AutoFormSwitch({
+ label,
+ isRequired,
+ field,
+ fieldConfigItem,
+ fieldProps,
+}: AutoFormInputComponentProps) {
+ return (
+
+ );
+}
diff --git a/src/components/auto-form/fields/textarea.tsx b/src/components/auto-form/fields/textarea.tsx
new file mode 100644
index 0000000..904c5df
--- /dev/null
+++ b/src/components/auto-form/fields/textarea.tsx
@@ -0,0 +1,27 @@
+import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
+import { Textarea } from "@/components/ui/textarea";
+import AutoFormLabel from "../common/label";
+import AutoFormTooltip from "../common/tooltip";
+import type { AutoFormInputComponentProps } from "../types";
+
+export default function AutoFormTextarea({
+ label,
+ isRequired,
+ fieldConfigItem,
+ fieldProps,
+}: AutoFormInputComponentProps) {
+ const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = fieldProps;
+ const showLabel = _showLabel === undefined ? true : _showLabel;
+ return (
+
+ {showLabel && (
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/auto-form/index.tsx b/src/components/auto-form/index.tsx
new file mode 100644
index 0000000..d7bdfff
--- /dev/null
+++ b/src/components/auto-form/index.tsx
@@ -0,0 +1,117 @@
+"use client";
+import { Form } from "@/components/ui/form";
+import type React from "react";
+import { useEffect } from "react";
+import { type DefaultValues, type FormState, useForm } from "react-hook-form";
+import type { z } from "zod";
+
+import { Button } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+import { zodResolver } from "@hookform/resolvers/zod";
+
+import AutoFormObject from "@/components/auto-form/fields/object";
+import type { Dependency, FieldConfig } from "@/components/auto-form/types";
+import {
+ type ZodObjectOrWrapped,
+ getDefaultValues,
+ getObjectFormSchema,
+} from "@/components/auto-form/utils";
+
+export function AutoFormSubmit({
+ children,
+ className,
+ disabled,
+}: {
+ children?: React.ReactNode;
+ className?: string;
+ disabled?: boolean;
+}) {
+ return (
+
+ );
+}
+
+function AutoForm({
+ formSchema,
+ values: valuesProp,
+ onValuesChange: onValuesChangeProp,
+ onParsedValuesChange,
+ onSubmit: onSubmitProp,
+ fieldConfig,
+ children,
+ className,
+ dependencies,
+}: {
+ formSchema: SchemaType;
+ values?: Partial>;
+ onValuesChange?: (values: Partial>) => void;
+ onParsedValuesChange?: (values: Partial>) => void;
+ onSubmit?: (values: z.infer) => void;
+ fieldConfig?: FieldConfig>;
+ children?:
+ | React.ReactNode
+ | ((formState: FormState>) => React.ReactNode);
+ className?: string;
+ dependencies?: Dependency>[];
+}) {
+ const objectFormSchema = getObjectFormSchema(formSchema);
+ const defaultValues: DefaultValues> | null =
+ getDefaultValues(objectFormSchema, fieldConfig);
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: defaultValues ?? undefined,
+ values: valuesProp,
+ });
+
+ function onSubmit(values: z.infer) {
+ const parsedValues = formSchema.safeParse(values);
+ if (parsedValues.success) {
+ onSubmitProp?.(parsedValues.data);
+ }
+ }
+
+ const values = form.watch();
+ // valuesString is needed because form.watch() returns a new object every time
+ const valuesString = JSON.stringify(values);
+
+ // biome-ignore lint/correctness/useExhaustiveDependencies:
+ useEffect(() => {
+ onValuesChangeProp?.(values);
+ const parsedValues = formSchema.safeParse(values);
+ if (parsedValues.success) {
+ onParsedValuesChange?.(parsedValues.data);
+ }
+ }, [valuesString]);
+
+ const renderChildren =
+ typeof children === "function"
+ ? children(form.formState as FormState>)
+ : children;
+
+ return (
+
+ );
+}
+
+export default AutoForm;
diff --git a/src/components/auto-form/types.ts b/src/components/auto-form/types.ts
new file mode 100644
index 0000000..db9ada8
--- /dev/null
+++ b/src/components/auto-form/types.ts
@@ -0,0 +1,71 @@
+import type React from "react";
+import type { ControllerRenderProps, FieldValues } from "react-hook-form";
+import type * as z from "zod";
+import type { INPUT_COMPONENTS } from "./config";
+
+export type FieldConfigItem = {
+ description?: React.ReactNode;
+ inputProps?: React.InputHTMLAttributes & {
+ showLabel?: boolean;
+ };
+ label?: string;
+ fieldType?: keyof typeof INPUT_COMPONENTS | React.FC;
+
+ renderParent?: (props: {
+ children: React.ReactNode;
+ }) => React.ReactElement | null;
+};
+
+export type FieldConfig>> = {
+ // If SchemaType.key is an object, create a nested FieldConfig, otherwise FieldConfigItem
+ [Key in keyof SchemaType]?: SchemaType[Key] extends object
+ ? FieldConfig>
+ : FieldConfigItem;
+};
+
+export enum DependencyType {
+ DISABLES = 0,
+ REQUIRES = 1,
+ HIDES = 2,
+ SETS_OPTIONS = 3,
+}
+
+type BaseDependency>> = {
+ sourceField: keyof SchemaType;
+ type: DependencyType;
+ targetField: keyof SchemaType;
+ when: (sourceFieldValue: any, targetFieldValue: any) => boolean;
+};
+
+export type ValueDependency>> =
+ BaseDependency & {
+ type: DependencyType.DISABLES | DependencyType.REQUIRES | DependencyType.HIDES;
+ };
+
+export type EnumValues = readonly [string, ...string[]];
+
+export type OptionsDependency>> =
+ BaseDependency & {
+ type: DependencyType.SETS_OPTIONS;
+
+ // Partial array of values from sourceField that will trigger the dependency
+ options: EnumValues;
+ };
+
+export type Dependency>> =
+ | ValueDependency
+ | OptionsDependency;
+
+/**
+ * A FormInput component can handle a specific Zod type (e.g. "ZodBoolean")
+ */
+export type AutoFormInputComponentProps = {
+ zodInputProps: React.InputHTMLAttributes;
+ field: ControllerRenderProps;
+ fieldConfigItem: FieldConfigItem;
+ label: string;
+ isRequired: boolean;
+ fieldProps: any;
+ zodItem: z.ZodAny;
+ className?: string;
+};
diff --git a/src/components/auto-form/utils.ts b/src/components/auto-form/utils.ts
new file mode 100644
index 0000000..4e72616
--- /dev/null
+++ b/src/components/auto-form/utils.ts
@@ -0,0 +1,167 @@
+import type React from "react";
+import type { DefaultValues } from "react-hook-form";
+import type { z } from "zod";
+import type { FieldConfig } from "./types";
+
+// TODO: This should support recursive ZodEffects but TypeScript doesn't allow circular type definitions.
+export type ZodObjectOrWrapped =
+ | z.ZodObject
+ | z.ZodEffects>;
+
+/**
+ * Beautify a camelCase string.
+ * e.g. "myString" -> "My String"
+ */
+export function beautifyObjectName(string: string) {
+ // if numbers only return the string
+ let output = string.replace(/([A-Z])/g, " $1");
+ output = output.charAt(0).toUpperCase() + output.slice(1);
+ return output;
+}
+
+/**
+ * Get the lowest level Zod type.
+ * This will unpack optionals, refinements, etc.
+ */
+export function getBaseSchema(
+ schema: ChildType | z.ZodEffects,
+): ChildType | null {
+ if (!schema) return null;
+ if ("innerType" in schema._def) {
+ return getBaseSchema(schema._def.innerType as ChildType);
+ }
+ if ("schema" in schema._def) {
+ return getBaseSchema(schema._def.schema as ChildType);
+ }
+
+ return schema as ChildType;
+}
+
+/**
+ * Get the type name of the lowest level Zod type.
+ * This will unpack optionals, refinements, etc.
+ */
+export function getBaseType(schema: z.ZodAny): string {
+ const baseSchema = getBaseSchema(schema);
+ return baseSchema ? baseSchema._def.typeName : "";
+}
+
+/**
+ * Search for a "ZodDefult" in the Zod stack and return its value.
+ */
+export function getDefaultValueInZodStack(schema: z.ZodAny): any {
+ const typedSchema = schema as unknown as z.ZodDefault;
+
+ if (typedSchema._def.typeName === "ZodDefault") {
+ return typedSchema._def.defaultValue();
+ }
+
+ if ("innerType" in typedSchema._def) {
+ return getDefaultValueInZodStack(typedSchema._def.innerType as unknown as z.ZodAny);
+ }
+ if ("schema" in typedSchema._def) {
+ return getDefaultValueInZodStack((typedSchema._def as any).schema as z.ZodAny);
+ }
+
+ return undefined;
+}
+
+/**
+ * Get all default values from a Zod schema.
+ */
+export function getDefaultValues>(
+ schema: Schema,
+ fieldConfig?: FieldConfig>,
+) {
+ if (!schema) return null;
+ const { shape } = schema;
+ type DefaultValuesType = DefaultValues>>;
+ const defaultValues = {} as DefaultValuesType;
+ if (!shape) return defaultValues;
+
+ for (const key of Object.keys(shape)) {
+ const item = shape[key] as z.ZodAny;
+
+ if (getBaseType(item) === "ZodObject") {
+ const defaultItems = getDefaultValues(
+ getBaseSchema(item) as unknown as z.ZodObject,
+ fieldConfig?.[key] as FieldConfig>,
+ );
+
+ if (defaultItems !== null) {
+ for (const defaultItemKey of Object.keys(defaultItems)) {
+ const pathKey = `${key}.${defaultItemKey}` as keyof DefaultValuesType;
+ defaultValues[pathKey] = defaultItems[defaultItemKey];
+ }
+ }
+ } else {
+ let defaultValue = getDefaultValueInZodStack(item);
+ if (
+ (defaultValue === null || defaultValue === "") &&
+ fieldConfig?.[key]?.inputProps
+ ) {
+ defaultValue = (fieldConfig?.[key]?.inputProps as unknown as any).defaultValue;
+ }
+ if (defaultValue !== undefined) {
+ defaultValues[key as keyof DefaultValuesType] = defaultValue;
+ }
+ }
+ }
+
+ return defaultValues;
+}
+
+export function getObjectFormSchema(schema: ZodObjectOrWrapped): z.ZodObject {
+ if (schema?._def.typeName === "ZodEffects") {
+ const typedSchema = schema as z.ZodEffects>;
+ return getObjectFormSchema(typedSchema._def.schema);
+ }
+ return schema as z.ZodObject;
+}
+
+/**
+ * Convert a Zod schema to HTML input props to give direct feedback to the user.
+ * Once submitted, the schema will be validated completely.
+ */
+export function zodToHtmlInputProps(
+ schema: z.ZodNumber | z.ZodString | z.ZodOptional | any,
+): React.InputHTMLAttributes {
+ if (["ZodOptional", "ZodNullable"].includes(schema._def.typeName)) {
+ const typedSchema = schema as z.ZodOptional;
+ return {
+ ...zodToHtmlInputProps(typedSchema._def.innerType),
+ required: false,
+ };
+ }
+ const typedSchema = schema as z.ZodNumber | z.ZodString;
+
+ if (!("checks" in typedSchema._def))
+ return {
+ required: true,
+ };
+
+ const { checks } = typedSchema._def;
+ const inputProps: React.InputHTMLAttributes = {
+ required: true,
+ };
+ const type = getBaseType(schema);
+
+ for (const check of checks) {
+ if (check.kind === "min") {
+ if (type === "ZodString") {
+ inputProps.minLength = check.value;
+ } else {
+ inputProps.min = check.value;
+ }
+ }
+ if (check.kind === "max") {
+ if (type === "ZodString") {
+ inputProps.maxLength = check.value;
+ } else {
+ inputProps.max = check.value;
+ }
+ }
+ }
+
+ return inputProps;
+}