diff --git a/src/components/form.tsx b/src/components/form.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/src/components/ui/auto-form/common/label.tsx b/src/components/ui/auto-form/common/label.tsx
new file mode 100644
index 0000000..d570830
--- /dev/null
+++ b/src/components/ui/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/ui/auto-form/common/tooltip.tsx b/src/components/ui/auto-form/common/tooltip.tsx
new file mode 100644
index 0000000..cafdb79
--- /dev/null
+++ b/src/components/ui/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/ui/auto-form/config.ts b/src/components/ui/auto-form/config.ts
new file mode 100644
index 0000000..be416ba
--- /dev/null
+++ b/src/components/ui/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/ui/auto-form/dependencies.ts b/src/components/ui/auto-form/dependencies.ts
new file mode 100644
index 0000000..4d9cada
--- /dev/null
+++ b/src/components/ui/auto-form/dependencies.ts
@@ -0,0 +1,57 @@
+import { FieldValues, UseFormWatch } from "react-hook-form";
+import { Dependency, DependencyType, EnumValues } from "./types";
+import * as z from "zod";
+
+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/ui/auto-form/fields/array.tsx b/src/components/ui/auto-form/fields/array.tsx
new file mode 100644
index 0000000..c66e972
--- /dev/null
+++ b/src/components/ui/auto-form/fields/array.tsx
@@ -0,0 +1,93 @@
+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, 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/ui/auto-form/fields/checkbox.tsx b/src/components/ui/auto-form/fields/checkbox.tsx
new file mode 100644
index 0000000..2b0121c
--- /dev/null
+++ b/src/components/ui/auto-form/fields/checkbox.tsx
@@ -0,0 +1,34 @@
+import { Checkbox } from "@/components/ui/checkbox";
+import { FormControl, FormItem } from "@/components/ui/form";
+import AutoFormTooltip from "../common/tooltip";
+import { AutoFormInputComponentProps } from "../types";
+import AutoFormLabel from "../common/label";
+
+export default function AutoFormCheckbox({
+ label,
+ isRequired,
+ field,
+ fieldConfigItem,
+ fieldProps,
+}: AutoFormInputComponentProps) {
+ return (
+
+ );
+}
diff --git a/src/components/ui/auto-form/fields/date.tsx b/src/components/ui/auto-form/fields/date.tsx
new file mode 100644
index 0000000..9c06cdc
--- /dev/null
+++ b/src/components/ui/auto-form/fields/date.tsx
@@ -0,0 +1,32 @@
+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 { AutoFormInputComponentProps } from "../types";
+
+export default function AutoFormDate({
+ label,
+ isRequired,
+ field,
+ fieldConfigItem,
+ fieldProps,
+}: AutoFormInputComponentProps) {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/ui/auto-form/fields/enum.tsx b/src/components/ui/auto-form/fields/enum.tsx
new file mode 100644
index 0000000..d5a68db
--- /dev/null
+++ b/src/components/ui/auto-form/fields/enum.tsx
@@ -0,0 +1,67 @@
+import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import * as z from "zod";
+import AutoFormLabel from "../common/label";
+import AutoFormTooltip from "../common/tooltip";
+import { 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/ui/auto-form/fields/file.tsx b/src/components/ui/auto-form/fields/file.tsx
new file mode 100644
index 0000000..058aab7
--- /dev/null
+++ b/src/components/ui/auto-form/fields/file.tsx
@@ -0,0 +1,67 @@
+import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { Trash2 } from "lucide-react";
+import { ChangeEvent, useState } from "react";
+import AutoFormLabel from "../common/label";
+import AutoFormTooltip from "../common/tooltip";
+import { 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/ui/auto-form/fields/input.tsx b/src/components/ui/auto-form/fields/input.tsx
new file mode 100644
index 0000000..33fcd36
--- /dev/null
+++ b/src/components/ui/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 { 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/ui/auto-form/fields/number.tsx b/src/components/ui/auto-form/fields/number.tsx
new file mode 100644
index 0000000..8637732
--- /dev/null
+++ b/src/components/ui/auto-form/fields/number.tsx
@@ -0,0 +1,31 @@
+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 { 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/ui/auto-form/fields/object.tsx b/src/components/ui/auto-form/fields/object.tsx
new file mode 100644
index 0000000..5ec122d
--- /dev/null
+++ b/src/components/ui/auto-form/fields/object.tsx
@@ -0,0 +1,183 @@
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from "@/components/ui/accordion";
+import { FormField } from "@/components/ui/form";
+import { useForm, useFormContext } from "react-hook-form";
+import * as z from "zod";
+import { DEFAULT_ZOD_HANDLERS, INPUT_COMPONENTS } from "../config";
+import { Dependency, FieldConfig, FieldConfigItem } from "../types";
+import {
+ beautifyObjectName,
+ getBaseSchema,
+ getBaseType,
+ zodToHtmlInputProps,
+} from "../utils";
+import AutoFormArray from "./array";
+import resolveDependencies from "../dependencies";
+
+function DefaultParent({ children }: { children: React.ReactNode }) {
+ return <>{children}>;
+}
+
+export default function AutoFormObject<
+ SchemaType extends z.ZodObject,
+>({
+ 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<
+ z.infer
+ >
+ }
+ 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/ui/auto-form/fields/radio-group.tsx b/src/components/ui/auto-form/fields/radio-group.tsx
new file mode 100644
index 0000000..fd192f0
--- /dev/null
+++ b/src/components/ui/auto-form/fields/radio-group.tsx
@@ -0,0 +1,63 @@
+import {
+ FormControl,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
+import * as z from "zod";
+import AutoFormLabel from "../common/label";
+import AutoFormTooltip from "../common/tooltip";
+import { 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/ui/auto-form/fields/switch.tsx b/src/components/ui/auto-form/fields/switch.tsx
new file mode 100644
index 0000000..24ea9ff
--- /dev/null
+++ b/src/components/ui/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 { AutoFormInputComponentProps } from "../types";
+
+export default function AutoFormSwitch({
+ label,
+ isRequired,
+ field,
+ fieldConfigItem,
+ fieldProps,
+}: AutoFormInputComponentProps) {
+ return (
+
+ );
+}
diff --git a/src/components/ui/auto-form/fields/textarea.tsx b/src/components/ui/auto-form/fields/textarea.tsx
new file mode 100644
index 0000000..2064551
--- /dev/null
+++ b/src/components/ui/auto-form/fields/textarea.tsx
@@ -0,0 +1,30 @@
+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 { 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/ui/auto-form/index.tsx b/src/components/ui/auto-form/index.tsx
new file mode 100644
index 0000000..13e6edc
--- /dev/null
+++ b/src/components/ui/auto-form/index.tsx
@@ -0,0 +1,115 @@
+"use client";
+import { Form } from "@/components/ui/form";
+import React from "react";
+import { DefaultValues, FormState, useForm } from "react-hook-form";
+import { z } from "zod";
+
+import { Button } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+import { zodResolver } from "@hookform/resolvers/zod";
+
+import AutoFormObject from "./fields/object";
+import { Dependency, FieldConfig } from "./types";
+import {
+ ZodObjectOrWrapped,
+ getDefaultValues,
+ getObjectFormSchema,
+} from "./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);
+
+ React.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/ui/auto-form/tests/basics.cy.tsx b/src/components/ui/auto-form/tests/basics.cy.tsx
new file mode 100644
index 0000000..9afd2a5
--- /dev/null
+++ b/src/components/ui/auto-form/tests/basics.cy.tsx
@@ -0,0 +1,244 @@
+import { z } from "zod";
+import AutoForm from "../index";
+
+describe("", () => {
+ it("renders fields", () => {
+ const formSchema = z.object({
+ username: z.string().min(2, {
+ message: "Username must be at least 2 characters.",
+ }),
+
+ password: z.string().describe("Your secure password").min(8, {
+ message: "Password must be at least 8 characters.",
+ }),
+ });
+
+ cy.mount();
+ cy.get("input[name=username]").should("exist");
+ cy.get("input[name=password]").should("exist");
+ });
+
+ it("renders fields with custom labels", () => {
+ const formSchema = z.object({
+ username: z.string().describe("Your username"),
+ });
+
+ cy.mount();
+
+ cy.get("label").contains("Your username");
+ });
+
+ it("generates default labels", () => {
+ const formSchema = z.object({
+ someFieldName: z.string(),
+ });
+
+ cy.mount();
+
+ cy.get("label").contains("Some Field Name");
+ });
+
+ it("allows setting custom field labels", () => {
+ const formSchema = z.object({
+ someFieldName: z.string(),
+ });
+
+ cy.mount(
+ ,
+ );
+
+ cy.get("label").contains("My field name");
+ });
+
+ it("allows setting custom field props", () => {
+ const formSchema = z.object({
+ username: z.string(),
+ });
+
+ cy.mount(
+ ,
+ );
+
+ cy.get("input[name=username]").should(
+ "have.attr",
+ "placeholder",
+ "Enter your username",
+ );
+ });
+
+ it("allows setting custom field type", () => {
+ const formSchema = z.object({
+ username: z.string(),
+ });
+
+ cy.mount(
+ ,
+ );
+
+ cy.get("input").should("have.attr", "type", "number");
+ });
+
+ it("can submit valid forms", () => {
+ const formSchema = z.object({
+ username: z.string(),
+ });
+
+ cy.mount(
+ {
+ expect(values).to.deep.equal({
+ username: "john",
+ });
+ }}
+ >
+
+ ,
+ );
+
+ cy.get("input[name=username]").type("john");
+ cy.get("button[type=submit]").click();
+ });
+
+ it("shows error for invalid forms", () => {
+ const formSchema = z.object({
+ username: z.string(),
+ });
+
+ cy.mount(
+ {
+ expect.fail("Should not be called.");
+ }}
+ >
+
+ ,
+ );
+
+ cy.get("button[type=submit]").click();
+ });
+
+ it("can set default values", () => {
+ const formSchema = z.object({
+ username: z.string().default("john"),
+ });
+
+ cy.mount();
+
+ cy.get("input[name=username]").should("have.value", "john");
+ });
+
+ it("can submit with default values", () => {
+ const formSchema = z.object({
+ username: z.string().default("john"),
+ });
+
+ cy.mount(
+ {
+ expect(values).to.deep.equal({
+ username: "john",
+ });
+ }}
+ >
+
+ ,
+ );
+
+ cy.get("button[type=submit]").click();
+ });
+
+ it("can set and submit optional values", () => {
+ const formSchema = z.object({
+ username: z.string().optional(),
+ });
+
+ cy.mount(
+ {
+ expect(values).to.deep.equal({
+ username: undefined,
+ });
+ }}
+ >
+
+ ,
+ );
+
+ cy.get("input[name=username]").should("have.value", "");
+ cy.get("button[type=submit]").click();
+ });
+
+ it("can add description", () => {
+ const formSchema = z.object({
+ username: z.string(),
+ });
+
+ cy.mount(
+ ,
+ );
+
+ cy.get("p").contains("Your username here");
+ });
+
+ it("can set default values on array", () => {
+ const formSchema = z.object({
+ arr: z.array(z.object({ name: z.string(), age: z.number() })).default([
+ { name: "Haykal", age: 21 },
+ { name: "John", age: 20 },
+ ]),
+ });
+
+ cy.mount();
+
+ //get button with text Arr
+ cy.get("button").contains("Arr").click();
+ cy.get("input[name='arr.0.name']").should("have.value", "Haykal");
+ cy.get("input[name='arr.0.age']").should("have.value", "21");
+ cy.get("input[name='arr.1.name']").should("have.value", "John");
+ cy.get("input[name='arr.1.age']").should("have.value", "20");
+ });
+
+ it("can set default value of number to 0", () => {
+ const formSchema = z.object({
+ number: z.number().default(0),
+ });
+
+ cy.mount();
+
+ cy.get("input[name='number']").should("have.value", "0");
+ });
+});
diff --git a/src/components/ui/auto-form/types.ts b/src/components/ui/auto-form/types.ts
new file mode 100644
index 0000000..b977467
--- /dev/null
+++ b/src/components/ui/auto-form/types.ts
@@ -0,0 +1,76 @@
+import { ControllerRenderProps, FieldValues } from "react-hook-form";
+import * as z from "zod";
+import { 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,
+ REQUIRES,
+ HIDES,
+ SETS_OPTIONS,
+}
+
+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<
+ SchemaType extends z.infer>,
+> = 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/ui/auto-form/utils.ts b/src/components/ui/auto-form/utils.ts
new file mode 100644
index 0000000..f8bfd4e
--- /dev/null
+++ b/src/components/ui/auto-form/utils.ts
@@ -0,0 +1,180 @@
+import React from "react";
+import { DefaultValues } from "react-hook-form";
+import { z } from "zod";
+import { 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<
+ ChildType extends z.ZodAny | z.AnyZodObject = z.ZodAny,
+>(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<
+ z.ZodNumber | z.ZodString
+ >;
+
+ 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;
+}
diff --git a/src/components/ui/date-picker.tsx b/src/components/ui/date-picker.tsx
new file mode 100644
index 0000000..4217c2e
--- /dev/null
+++ b/src/components/ui/date-picker.tsx
@@ -0,0 +1,46 @@
+"use client";
+import { format } from "date-fns";
+import { Calendar as CalendarIcon } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+import { Calendar } from "@/components/ui/calendar";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { forwardRef } from "react";
+
+export const DatePicker = forwardRef<
+ HTMLDivElement,
+ {
+ date?: Date;
+ setDate: (date?: Date) => void;
+ }
+>(function DatePickerCmp({ date, setDate }, ref) {
+ return (
+
+
+
+
+
+
+
+
+ );
+});