Aura Design System

Form

A comprehensive form system with schema-based validation, error handling, and field management. Built with Radix UI primitives and AJV validation, supporting text inputs, textareas, selects, checkboxes, switches, and checkbox groups with automatic error propagation and touch state management.

Preview

Loading...
import { useRef } from "react";
import * as React from "react";
import {
  Form,
  FormField,
  FormSubmit,
  FormSwitch,
  FormCheckbox,
  FormCheckboxGroup,
  FormAlert,
} from "@/components/ui/Form";
import { Input } from "@/components/ui/Input";
import { FormFieldCombobox } from "@/components/FormFieldCombobox";
import { FormFieldSelect } from "@/components/FormFieldSelect";
import { FormFieldSignaturePad } from "@/components/FormFieldSignaturePad";
import { FormFieldSortableList } from "@/components/FormFieldSortableList";
import { FormFieldEditor } from "@/components/FormFieldEditor";
import { useFormDynamic } from "@/hooks/use-dynamic-form";
import { validateFormData } from "@/utils/web-validation";

export function FormDemo() {
  return {
  const formRef = useRef<HTMLFormElement>(null);

  const formData = useFormDynamic({
    name: "text",
    email: "text",
  });

  const { name, email } = formData.getFields();

  const { isValid, errors } = validateFormData(
    defaultSchema,
    formData.getValues()
  );
  const formErrors = errors || undefined;

  const handleOnSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    formData.setFetchStatus("loading");

    if (!isValid) {
      formData.setFetchStatus("error");
      formData.touchForm();
      formData.setError("Please complete all required fields");
      return;
    }

    // Simulate API call
    await new Promise((resolve) => setTimeout(resolve, 1000));
    formData.setFetchStatus("success");
    console.log("Form submitted:", formData.getValues());
  };

  const formDataForAlert = {
    fetchStatus: formData.fetchStatus,
    error: formData.error,
  };

  return (
    <Form
      ref={formRef}
      onSubmit={handleOnSubmit}
      errors={formErrors}
      id="form-default"
      className="flex flex-col gap-1"
    >
      <FormAlert formData={formDataForAlert} />
      <FormField field={name} label="Name *">
        <Input type="text" placeholder="Enter your name" />
      </FormField>
      <FormField field={email} label="Email *">
        <Input type="email" placeholder="Enter your email" />
      </FormField>
      <FormSubmit
        fetchStatus={formData.fetchStatus}
        buttonProps={{ children: "Submit" }}
        form="form-default"
      />
    </Form>
  );
};

Installation

Make sure that namespace is set in your component.json file. Namespace docs: Learn more about namespaces

pnpm dlx shadcn@latest add @aura/form

Manual

Install the following dependencies:

pnpm install @radix-ui/react-icons ajv radix-ui

Copy and paste the alert status component into your components/AlertStatus.tsx file.

import React from "react";
import {
  InfoCircledIcon,
  CheckCircledIcon,
  ExclamationTriangleIcon,
  CrossCircledIcon,
  QuoteIcon,
} from "@radix-ui/react-icons";

import {
  Alert,
  AlertContent,
  AlertTitle,
  AlertDescription,
  AlertIcon,
} from "@/components/ui/Alert";

export type AlertProps = {
  status?: "info" | "success" | "warning" | "danger" | "other";
  title?: React.ReactNode;
  description?: React.ReactNode;
  icon?: React.ReactNode;
  showIcon?: boolean;
};

const AlertStatus = ({
  status = "other",
  title,
  description,
  icon,
  showIcon = true,
  ...props
}: AlertProps) => {
  // Map status to Alert variant
  const variantMap: Record<
    "info" | "success" | "warning" | "danger" | "other",
    "info" | "success" | "warning" | "danger" | "default"
  > = {
    info: "info",
    success: "success",
    warning: "warning",
    danger: "danger",
    other: "default",
  };

  // Map status to default icons
  const iconMap = {
    info: InfoCircledIcon,
    success: CheckCircledIcon,
    warning: ExclamationTriangleIcon,
    danger: CrossCircledIcon,
    other: QuoteIcon,
  };

  const variant = variantMap[status];
  const DefaultIcon = iconMap[status];

  return (
    <Alert variant={variant} {...props}>
      {showIcon && (
        <AlertIcon>
          {icon ? icon : <DefaultIcon />}
        </AlertIcon>
      )}
      <AlertContent>
        {title && <AlertTitle>{title}</AlertTitle>}
        {description && <AlertDescription>{description}</AlertDescription>}
      </AlertContent>
    </Alert>
  );
};

export default AlertStatus;

Copy and paste the checkbox component into your components/ui/Checkbox.tsx file.

"use client";
/**
 * @description A control that allows the user to toggle between checked and not checked.
 */
import * as React from "react";
import { Checkbox as CheckboxRadix } from "radix-ui";
import { CheckIcon } from "@radix-ui/react-icons";

import { cn } from "@/utils/class-names";

function Checkbox({
  className,
  ...props
}: React.ComponentProps<typeof CheckboxRadix.Root>) {
  return (
    <CheckboxRadix.Root
      data-slot="checkbox"
      className={cn(
        "border border-gray-a6 flex size-1.5 items-center justify-center rounded outline-none hover:bg-accent-2 cursor-pointer",
        className
      )}
      {...props}
    >
      <CheckboxIndicator />
    </CheckboxRadix.Root>
  );
}

function CheckboxIndicator({
  className,
  ...props
}: React.ComponentProps<typeof CheckboxRadix.Indicator>) {
  return (
    <CheckboxRadix.Indicator {...props}>
      <CheckIcon className={cn(className, "text-accent-9")} />
    </CheckboxRadix.Indicator>
  );
}

interface CheckboxGroupProps extends React.HTMLAttributes<HTMLDivElement> {
  value?: string[];
  onValueChange?: (value: string[]) => void;
  defaultValue?: string[];
}

function CheckboxGroup({
  className,
  value,
  onValueChange,
  defaultValue,
  children,
  ...props
}: CheckboxGroupProps) {
  const [internalValue, setInternalValue] = React.useState<string[]>(
    defaultValue ?? []
  );
  const controlledValue = value ?? internalValue;

  const handleValueChange = React.useCallback(
    (itemValue: string, checked: boolean) => {
      const newValue = checked
        ? [...controlledValue, itemValue]
        : controlledValue.filter((v) => v !== itemValue);

      if (onValueChange) {
        onValueChange(newValue);
      } else {
        setInternalValue(newValue);
      }
    },
    [controlledValue, onValueChange]
  );

  return (
    <div
      data-slot="checkbox-group"
      className={cn("flex flex-col gap-1", className)}
      {...props}
    >
      {React.Children.map(children, (child) => {
        if (
          React.isValidElement<CheckboxGroupItemProps>(child) &&
          child.type === CheckboxGroupItem
        ) {
          const childProps = child.props;
          return React.cloneElement(child, {
            ...childProps,
            checked: controlledValue.includes(childProps.value),
            onCheckedChange: (checked: boolean) =>
              handleValueChange(childProps.value, checked),
          });
        }
        return child;
      })}
    </div>
  );
}

interface CheckboxGroupItemProps
  extends Omit<React.ComponentProps<typeof CheckboxRadix.Root>, "onCheckedChange"> {
  value: string;
  label?: React.ReactNode;
  description?: React.ReactNode;
  onCheckedChange?: (checked: boolean) => void;
}

function CheckboxGroupItem({
  className,
  value,
  label,
  description,
  id,
  ...props
}: CheckboxGroupItemProps) {
  const checkboxId = id || `checkbox-${value}`;

  return (
    <div
      data-slot="checkbox-group-item"
      className={cn("flex items-start gap-1", className)}
    >
      <Checkbox id={checkboxId} {...props} />
      {(label || description) && (
        <div className="flex flex-col gap-1">
          {label && (
            <label
              htmlFor={checkboxId}
              className="text-sm font-medium leading-none cursor-pointer"
            >
              {label}
            </label>
          )}
          {description && (
            <p className="text-sm text-gray-a11 m-0">{description}</p>
          )}
        </div>
      )}
    </div>
  );
}

export { Checkbox, CheckboxIndicator, CheckboxGroup, CheckboxGroupItem };

Copy and paste the class names utility into your utils/class-names.ts file.

import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

Copy and paste the radio group component into your components/ui/RadioGroup.tsx file.

"use client";

import * as React from "react";
import { RadioGroup as RadioGroupPrimitive } from "radix-ui";

import { cn } from "@/utils/class-names";

function RadioGroup({
  className,
  ...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
  return (
    <RadioGroupPrimitive.Root
      data-slot="radio-group"
      className={cn(className, "cursor-pointer")}
      {...props}
    />
  );
}

function RadioGroupItem({
  className,
  ...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
  return (
    <RadioGroupPrimitive.Item
      data-slot="radio-group-item"
      className={cn(
        className,
        "size-1.5 cursor-pointer rounded-full border border-gray-a6 hover:bg-accent-2"
      )}
      {...props}
    >
      <RadioGroupPrimitive.Indicator
        data-slot="radio-group-indicator"
        className="relative flex size-full items-center justify-center after:block after:size-0.5 after:rounded-full after:bg-accent-9"
      />
    </RadioGroupPrimitive.Item>
  );
}

export { RadioGroup, RadioGroupItem };

Copy and paste the switch component into your components/ui/Switch.tsx file.

"use client";
/**
 * @description A control that allows the user to toggle between on and off states.
 */
import * as React from "react";
import { Switch as SwitchPrimitive } from "radix-ui";

import { cn } from "@/utils/class-names";

function Switch({
  className,
  ...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
  return (
    <SwitchPrimitive.Root
      data-slot="switch"
      className={cn(
        "relative h-1.5 w-2.5 cursor-pointer rounded-full outline-none bg-accent-4 data-[state=checked]:bg-accent-10 border border-gray-a6 hover:bg-accent-5",
        className
      )}
      {...props}
    >
      <SwitchThumb />
    </SwitchPrimitive.Root>
  );
}

function SwitchThumb({
  className,
  ...props
}: React.ComponentProps<typeof SwitchPrimitive.Thumb>) {
  return (
    <SwitchPrimitive.Thumb
      data-slot="switch-thumb"
      className={cn(
        "block size-1 translate-x-[3.5px] rounded-full bg-gray-1 shadow-md transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[15.5px]",
        className
      )}
      {...props}
    />
  );
}

export { Switch, SwitchThumb };

Copy and paste the use dynamic form hook into your hooks/use-dynamic-form.ts file.

import { useState } from "react";

// Define the possible field types for the form.
type FieldType = "text" | "textarea" | "select" | "checkbox";

// Interface to define the structure of the initial values object.
// It maps field names (string) to their respective field types.
interface useFormDynamicProps {
  [key: string]: FieldType;
}

// Object to resolve the initial value based on the field type.
const initialValueResolver = {
  text: "",
  textarea: "",
  select: "",
  checkbox: false,
};

/**
 * Custom hook to manage the state of input fields in a form.
 * @param initialValues - An object where keys are field names and values are field types.
 * @returns An object containing the current values, errors, touch states, and functions to manage them.
 */
const useInputValueFields = (initialValues: useFormDynamicProps = {}) => {
  // Resolve initial values based on the provided types.
  const resolvedInitialValues = Object.entries(initialValues).reduce(
    (acc, [key, type]) => {
      acc[key] = initialValueResolver[type];
      return acc;
    },
    {} as Record<string, string | boolean>
  );

  // State to hold the current values of the form fields.
  const [value, setValue] = useState<Record<string, string | boolean>>(
    resolvedInitialValues
  );
  // State to hold any errors related to the form fields.
  const [error, setError] = useState<string | null>(null);
  // State to track if a field has been touched (focused and blurred).
  const [touch, setTouch] = useState<Record<string, boolean>>({});

  return {
    types: initialValues, // The types of the fields.
    value, // The current values of the fields.
    error, // Any errors associated with the fields.
    touch, // The touch state of the fields.
    setTouch, // Function to update the touch state.
    setValue, // Function to update the field values.
    setError, // Function to update the error state.
  };
};

// Type definition for the props of a single field.
export type FieldProps = {
  name: string; // The name of the field.
  type: FieldType; // The type of the field.
  value: string | boolean; // The current value of the field.
  setValue: (value: string | boolean) => void; // Function to set the value of the field.
  onChange: (
    event: React.ChangeEvent<
      HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
    >
  ) => void; // Function to handle changes in the field.
  onCheckedChange: React.ChangeEventHandler<HTMLInputElement> &
    ((checked: boolean) => void); // Function to handle changes in checkbox fields.
  touch: boolean; // Whether the field has been touched.
  setTouch: (value: boolean) => void; // Function to set the touch state of the field.
  reset: () => void; // Function to reset the field to its default value.
};

/**
 * Custom hook to manage a dynamic form with multiple fields.
 * @param initialValues - An object defining the initial field types.
 * @returns An object containing form state, field management functions, and form-level actions.
 */
export const useFormDynamic = (
  initialValues: useFormDynamicProps,
  formGeneralRef?: React.RefObject<HTMLFormElement | null>
) => {
  // State to track the status of a fetch operation (e.g., submitting the form).
  const [fetchStatus, setFetchStatus] = useState<
    "idle" | "loading" | "success" | "error"
  >("idle");
  // Use the useInputValueFields hook to manage the fields.
  const fields = useInputValueFields(initialValues);

  /**
   * Updates a specific field's value and touch state.
   * @param name - The name of the field to update.
   * @param updates - An object containing the new value and/or touch state.
   */
  const updateField = (
    name: string,
    updates: { value?: string | boolean; touch?: boolean }
  ) => {
    // Update the field's value.
    fields.setValue((prev) => {
      const newValues = { ...prev, [name]: updates.value };
      return newValues;
    });

    // Update the field's touch state.
    fields.setTouch((prev) => ({ ...prev, [name]: updates.touch }));
  };

  /**
   * Returns the props for a specific field.
   * @param name - The name of the field.
   * @returns An object containing the field's props.
   */
  const field = (name: string): FieldProps => {
    const fieldType = fields.types[name];
    const defaultValue = initialValueResolver[fieldType];

    // Handles changes in the field's value.
    const handleChange = (
      event: React.ChangeEvent<
        HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
      >
    ) => {
      const value = event.target.value;

      updateField(name, {
        value,
        touch: true,
      });
    };

    // Handles changes in checkbox fields.
    const handleOnCheckedChange = (event: boolean): void => {
      updateField(name, {
        value: event,
        touch: true,
      });
    };

    // Sets the field's value in the DOM and updates the state.
    const setFormFieldValue = (
      value: string | boolean,
      formRef = formGeneralRef
    ): void => {
      if (formRef) {

        const input = formRef?.current?.querySelector(`[name="${name}"]`) as
          | HTMLInputElement
          | HTMLTextAreaElement
          | HTMLSelectElement
          | null;

        if (input) {
          if (input instanceof HTMLInputElement && input.type === "checkbox") {
            (input as HTMLInputElement).checked = value as boolean;
          } else {
            input.value = String(value);
          }
        }
      }
      updateField(name, {
        value: value,
      });
    };

    return {
      name,
      type: fields.types[name],
      value: fields.value[name] ?? defaultValue,
      setValue: setFormFieldValue,
      onChange: handleChange,
      onCheckedChange:
        handleOnCheckedChange as React.ChangeEventHandler<HTMLInputElement> &
          ((checked: boolean) => void),
      touch: fields.touch[name] ?? false,
      setTouch: (value: boolean) => updateField(name, { touch: value }),
      reset: () =>
        updateField(name, {
          value: defaultValue,
          touch: false,
        }),
    };
  };

  /**
   * Returns an object containing the props for all fields.
   * @returns An object where keys are field names and values are their props.
   */
  const getFields = () => {
    return Object.keys(fields.value).reduce(
      (acc, key) => {
        acc[key] = field(key);
        return acc;
      },
      {} as Record<string, ReturnType<typeof field>>
    );
  };

  /**
   * Resets the form to its initial state.
   * @param formRef - A ref to the form element.
   * @param initialValues - The initial values for the form fields.
   */
  const resetForm = (
    formRef: React.RefObject<HTMLFormElement>,
    initialValues?: Record<string, string | boolean>
  ): void => {
    const fields = getFields();

    for (const field in fields) {
      fields[field].reset();
      fields[field].setFormFieldValue(
        formRef,
        initialValues?.[field] ?? initialValueResolver[fields[field].type]
      );
    }
  };

  /**
   * Touches all fields in the form.
   */
  const touchForm = () => {
    const fields = getFields();
    for (const field in fields) {
      updateField(field, { value: fields[field].value, touch: true });
    }
  };

  /**
   * Returns an object containing the current values of all fields.
   * @returns An object where keys are field names and values are their current values.
   */
  const getValues = () => {
    return Object.keys(fields.value).reduce(
      (acc, key) => {
        acc[key] = fields.value[key];
        return acc;
      },
      {} as Record<string, string | boolean>
    );
  };

  return {
    ...fields, // Spread the fields object to include its properties.
    resetForm, // Function to reset the form.
    touchForm, // Function to touch all fields.
    field, // Function to get the props for a specific field.
    getFields, // Function to get the props for all fields.
    getValues, // Function to get the current values of all fields.
    fetchStatus, // The current fetch status.
    setFetchStatus, // Function to set the fetch status.
  };
};

Copy and paste the Form component into your components/ui/Form.tsx file.

import * as React from "react";
import { ErrorObject } from "ajv";
import { Form as FormRadix } from "radix-ui";
import { ChevronDownIcon, SymbolIcon } from "@radix-ui/react-icons";

import { FieldProps } from "@/hooks/use-dynamic-form";
import AlertStatus from "@/components/AlertStatus";
import Button, { ButtonProps } from "@/components/ui/Button";
import { cn } from "@/utils/class-names";
import {
  Checkbox,
  CheckboxGroup,
  CheckboxGroupItem,
} from "@/components/ui/Checkbox";
import { Switch } from "@/components/ui/Switch";
import { RadioGroup, RadioGroupItem } from "@/components/ui/RadioGroup";

interface FormProps extends FormRadix.FormProps {
  errors?: ErrorObject<string, Record<string, any>, unknown>[];
}
export const Form = React.forwardRef<HTMLFormElement, FormProps>(
  ({ children, errors, ...props }, forwardedRef) => {
    const childrenArray = React.Children.toArray(children);

    const processChildren = (
      children: React.ReactNode[]
    ): React.ReactNode[] => {
      return children.map((child) => {
        if (!React.isValidElement(child)) {
          return child;
        }

        if (child.props?.field) {
          const fieldErrors = errors?.filter(
            (error) => error.instancePath.slice(1) === child.props?.field?.name
          );

          return React.cloneElement(child as React.ReactElement, {
            ...child.props,
            errors: fieldErrors,
          });
        }

        if (child.props?.children) {
          const processedChildren = processChildren(
            React.Children.toArray(child.props.children)
          );
          return React.cloneElement(child as React.ReactElement, {
            ...child.props,
            children: processedChildren,
          });
        }

        return child;
      });
    };

    return (
      <FormRadix.Root {...props} ref={forwardedRef}>
        {processChildren(childrenArray)}
      </FormRadix.Root>
    );
  }
);

interface FormSubmitProps extends FormRadix.FormSubmitProps {
  buttonProps?: ButtonProps;
  fetchStatus?: "idle" | "loading" | "success" | "error";
}

export const FormSubmit = React.forwardRef<HTMLButtonElement, FormSubmitProps>(
  ({ buttonProps, fetchStatus, ...props }, forwardedRef) => {
    return (
      <FormRadix.Submit {...props} ref={forwardedRef} asChild>
        <Button
          {...buttonProps}
          isLoading={fetchStatus === "loading"}
          isLoadingText={
            <>
              <SymbolIcon className="icon animate-spin" />
            </>
          }
          className="min-w-10"
        />
      </FormRadix.Submit>
    );
  }
);

interface FormFieldProps extends Partial<FormRadix.FormFieldProps> {
  label: React.ReactNode;
  labelProps?: FormRadix.FormLabelProps;
  controlProps?: FormRadix.FormControlProps;
  field?: FieldProps;
  errors?: ErrorObject<string, Record<string, any>, unknown>[];
}

export const FormField = React.forwardRef<HTMLDivElement, FormFieldProps>(
  (
    { labelProps, label, controlProps, children, field, errors, ...props },
    forwardedRef
  ) => {
    const classNameConnect: string[] = ["flex flex-col gap-0.5"];
    const hasError = field.touch && errors && errors.length > 0;
    const hasSelect = React.Children.toArray(children).some(
      (child: any) => child?.type === "select"
    );

    if (props.className) {
      classNameConnect.push(props.className);
    }

    const name = props.name || field?.name;

    return (
      <FormRadix.Field
        className={classNameConnect.join(" ")}
        name={name}
        {...props}
        serverInvalid={hasError}
        ref={forwardedRef}
      >
        {label && <FormRadix.Label {...labelProps}>{label}</FormRadix.Label>}
        <div className="relative ">
          <FormRadix.Control
            {...controlProps}
            onChange={field?.onChange}
            asChild={Boolean(children)}
          >
            {children}
          </FormRadix.Control>
          {hasSelect && (
            <div className="absolute top-1/2 -translate-y-1/2 right-2 pointer-events-none">
              <ChevronDownIcon className="icon" />
            </div>
          )}
        </div>

        {hasError &&
          errors?.map((error, index) => (
            <FormRadix.Message className="text-warning-contrast" key={index}>
              {error.message}
            </FormRadix.Message>
          ))}
      </FormRadix.Field>
    );
  }
);

interface FormSwitchProps extends Partial<FormRadix.FormFieldProps> {
  id?: string;
  label: React.ReactNode;
  labelProps?: FormRadix.FormLabelProps;
  controlProps?: FormRadix.FormControlProps;
  field?: FieldProps;
  errors?: ErrorObject<string, Record<string, any>, unknown>[];
}

export const FormSwitch = React.forwardRef<HTMLDivElement, FormSwitchProps>(
  (
    { labelProps, label, controlProps, field, children, id, errors, ...props },
    forwardedRef
  ) => {
    const hasError = field.touch && errors && errors.length > 0;
    const idConnect = id ? id : React.useId();
    const name = props.name || field?.name;

    return (
      <FormRadix.Field
        {...props}
        ref={forwardedRef}
        name={name}
        serverInvalid={hasError}
      >
        <div className="flex items-center gap-1">
          {label && (
            <FormRadix.Label htmlFor={idConnect}>{label}</FormRadix.Label>
          )}
          <Switch
            id={idConnect}
            checked={Boolean(field?.value)}
            onCheckedChange={field?.onCheckedChange}
          />
        </div>
        <FormRadix.Control
          value="on"
          type="checkbox"
          checked={Boolean(field?.value)}
          onChange={field?.onCheckedChange}
          className="border-0 absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap break-normal clip-rect hidden"
        />
        {hasError &&
          errors?.map((error, index) => (
            <FormRadix.Message className="text-warning-contrast" key={index}>
              {error.message}
            </FormRadix.Message>
          ))}
      </FormRadix.Field>
    );
  }
);

interface FormCheckboxProps extends Partial<FormRadix.FormFieldProps> {
  id?: string;
  label: React.ReactNode;
  labelProps?: FormRadix.FormLabelProps;
  controlProps?: FormRadix.FormControlProps;
  field?: FieldProps;
  errors?: ErrorObject<string, Record<string, any>, unknown>[];
}

export const FormCheckbox = React.forwardRef<HTMLDivElement, FormCheckboxProps>(
  (
    { labelProps, label, controlProps, field, children, id, errors, ...props },
    forwardedRef
  ) => {
    const hasError = field.touch && errors && errors.length > 0;
    const idConnect = id ? id : React.useId();
    const name = props.name || field?.name;
    return (
      <FormRadix.Field
        {...props}
        ref={forwardedRef}
        name={name}
        serverInvalid={hasError}
      >
        <div className="flex items-center gap-1">
          <div>
            <Checkbox
              id={idConnect}
              className="border border-gray-a6 flex size-1.5 items-center justify-center rounded outline-none hover:bg-accent-2"
              checked={Boolean(field?.value)}
              onCheckedChange={field?.onCheckedChange}
            />
          </div>
          {label && (
            <FormRadix.Label htmlFor={idConnect}>{label}</FormRadix.Label>
          )}
        </div>
        <FormRadix.Control
          value="on"
          type="checkbox"
          checked={Boolean(field?.value)}
          onChange={field?.onCheckedChange}
          className="border-0 absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap break-normal clip-rect hidden"
        />
        {hasError &&
          errors?.map((error, index) => (
            <FormRadix.Message className="text-warning-contrast" key={index}>
              {error.message}
            </FormRadix.Message>
          ))}
      </FormRadix.Field>
    );
  }
);

export interface CheckboxGroupOption {
  value: string;
  label: React.ReactNode;
  description?: React.ReactNode;
  disabled?: boolean;
}

export interface RadioGroupOption {
  value: string;
  label: React.ReactNode;
  description?: React.ReactNode;
  disabled?: boolean;
}

interface FormCheckboxGroupProps extends Partial<FormRadix.FormFieldProps> {
  label?: React.ReactNode;
  labelProps?: FormRadix.FormLabelProps;
  controlProps?: FormRadix.FormControlProps;
  field?: FieldProps;
  errors?: ErrorObject<string, Record<string, any>, unknown>[];
  options: CheckboxGroupOption[];
  className?: string;
}

export const FormCheckboxGroup = React.forwardRef<
  HTMLDivElement,
  FormCheckboxGroupProps
>(
  (
    {
      label,
      labelProps,
      controlProps,
      field,
      errors,
      options,
      className,
      ...props
    },
    forwardedRef
  ) => {
    const hasError = field?.touch && errors && errors.length > 0;
    const name = props.name || field?.name || "";
    const fieldValue = Array.isArray(field?.value)
      ? (field?.value as string[])
      : [];

    const handleValueChange = React.useCallback(
      (value: string[]) => {
        if (field?.setValue) {
          // Checkbox groups use arrays, but field.setValue expects string | boolean
          // We'll cast it since checkbox groups specifically need array values
          field.setValue(value as any);
        }
      },
      [field]
    );

    return (
      <FormRadix.Field
        {...props}
        ref={forwardedRef}
        name={name}
        serverInvalid={hasError}
        className={className}
      >
        {label && (
          <div className="mb-1">
            <FormRadix.Label {...labelProps}>{label}</FormRadix.Label>
          </div>
        )}
        <FormRadix.Control {...controlProps} asChild>
          <CheckboxGroup
            value={fieldValue}
            onValueChange={handleValueChange}
            className="flex flex-col gap-1"
          >
            {options.map((option) => (
              <CheckboxGroupItem
                key={option.value}
                value={option.value}
                label={option.label}
                description={option.description}
                disabled={option.disabled}
              />
            ))}
          </CheckboxGroup>
        </FormRadix.Control>
        {hasError &&
          errors?.map((error, index) => (
            <FormRadix.Message className="text-warning-contrast" key={index}>
              {error.message}
            </FormRadix.Message>
          ))}
      </FormRadix.Field>
    );
  }
);

interface FormRadioGroupProps extends Partial<FormRadix.FormFieldProps> {
  label?: React.ReactNode;
  labelProps?: FormRadix.FormLabelProps;
  controlProps?: FormRadix.FormControlProps;
  field?: FieldProps;
  errors?: ErrorObject<string, Record<string, any>, unknown>[];
  options: RadioGroupOption[];
  className?: string;
}

export const FormRadioGroup = React.forwardRef<
  HTMLDivElement,
  FormRadioGroupProps
>(
  (
    {
      label,
      labelProps,
      controlProps,
      field,
      errors,
      options,
      className,
      ...props
    },
    forwardedRef
  ) => {
    const hasError = field?.touch && errors && errors.length > 0;
    const name = props.name || field?.name || "";
    const fieldValue = field?.value ? String(field.value) : "";

    const handleValueChange = React.useCallback(
      (value: string) => {
        if (field?.setValue) {
          field.setValue(value);
        }
      },
      [field]
    );

    return (
      <FormRadix.Field
        {...props}
        ref={forwardedRef}
        name={name}
        serverInvalid={hasError}
        className={className}
      >
        {label && (
          <div className="mb-1">
            <FormRadix.Label {...labelProps}>{label}</FormRadix.Label>
          </div>
        )}
        <FormRadix.Control {...controlProps} asChild>
          <RadioGroup
            value={fieldValue}
            onValueChange={handleValueChange}
            className="flex flex-col gap-0.5"
          >
            {options.map((option) => {
              const optionId = `${name}-${option.value}`;
              return (
                <div
                  key={option.value}
                  className={cn(
                    "flex items-start gap-1",
                    option.disabled && "opacity-50 cursor-not-allowed"
                  )}
                >
                  <RadioGroupItem
                    value={option.value}
                    id={optionId}
                    disabled={option.disabled}
                  />
                  <div className="flex flex-col gap-0.5">
                    <label
                      htmlFor={optionId}
                      className="text-sm font-medium leading-none cursor-pointer"
                    >
                      {option.label}
                    </label>
                    {option.description && (
                      <p className="text-sm text-gray-11 m-0">
                        {option.description}
                      </p>
                    )}
                  </div>
                </div>
              );
            })}
          </RadioGroup>
        </FormRadix.Control>
        {hasError &&
          errors?.map((error, index) => (
            <FormRadix.Message className="text-warning-contrast" key={index}>
              {error.message}
            </FormRadix.Message>
          ))}
      </FormRadix.Field>
    );
  }
);

interface FormAlertProps {
  formData: any;
}

export const FormAlert = ({ formData, ...props }: FormAlertProps) => {
  if (formData.fetchStatus !== "error") {
    return null;
  }

  if (!formData.error) {
    return null;
  }

  return (
    <AlertStatus status="danger" {...props} description={formData.error} />
  );
};

Usage

MultipleFields

export const MultipleFields = () => {
  const formRef = useRef<HTMLFormElement>(null);

  const formData = useFormDynamic({
    name: "text",
    email: "text",
    message: "textarea",
    country: "select",
  });

  const { name, email, message, country } = formData.getFields();

  const { isValid, errors } = validateFormData(
    multipleFieldsSchema,
    formData.getValues()
  );
  const formErrors = errors || undefined;

  const handleOnSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    formData.setFetchStatus("loading");

    if (!isValid) {
      formData.setFetchStatus("error");
      formData.touchForm();
      formData.setError("Please complete all required fields");
      return;
    }

    // Simulate API call
    await new Promise((resolve) => setTimeout(resolve, 1000));
    formData.setFetchStatus("success");
    console.log("Form submitted:", formData.getValues());
  };

  const formDataForAlert = {
    fetchStatus: formData.fetchStatus,
    error: formData.error,
  };

  return (
    <Form
      ref={formRef}
      onSubmit={handleOnSubmit}
      errors={formErrors}
      id="form-multiple"
      className="flex flex-col gap-1"
    >
      <FormAlert formData={formDataForAlert} />
      <FormField field={name} label="Name *">
        <Input type="text" placeholder="Enter your name" />
      </FormField>
      <FormField field={email} label="Email *">
        <Input type="email" placeholder="Enter your email" />
      </FormField>
      <FormField field={message} label="Message">
        <textarea placeholder="Enter your message" rows={4} />
      </FormField>
      <FormField field={country} label="Country">
        <select>
          <option value="">Select a country</option>
          <option value="us">United States</option>
          <option value="uk">United Kingdom</option>
          <option value="ca">Canada</option>
          <option value="au">Australia</option>
        </select>
      </FormField>
      <FormSubmit
        fetchStatus={formData.fetchStatus}
        buttonProps={{ children: "Submit" }}
        form="form-multiple"
      />
    </Form>
  );
};

WithErrors

export const WithErrors = () => {
  const formRef = useRef<HTMLFormElement>(null);

  const formData = useFormDynamic({
    name: "text",
    email: "text",
  });

  const { name, email } = formData.getFields();

  const { isValid, errors } = validateFormData(
    defaultSchema,
    formData.getValues()
  );
  const formErrors = errors || undefined;

  const handleOnSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    formData.setFetchStatus("loading");

    if (!isValid) {
      formData.setFetchStatus("error");
      formData.touchForm();
      formData.setError("Please fix the validation errors");
      return;
    }

    formData.setFetchStatus("success");
  };

  const formDataForAlert = {
    fetchStatus: formData.fetchStatus,
    error: formData.error,
  };

  return (
    <Form
      ref={formRef}
      onSubmit={handleOnSubmit}
      errors={formErrors}
      id="form-errors"
      className="flex flex-col gap-1"
    >
      <FormAlert formData={formDataForAlert} />
      <FormField field={name} label="Name *">
        <Input type="text" placeholder="Enter your name" />
      </FormField>
      <FormField field={email} label="Email *">
        <Input type="email" placeholder="Enter your email" />
      </FormField>
      <FormSubmit
        fetchStatus={formData.fetchStatus}
        buttonProps={{ children: "Submit" }}
        form="form-errors"
      />
    </Form>
  );
};

WithSwitch

export const WithSwitch = () => {
  const formRef = useRef<HTMLFormElement>(null);

  const formData = useFormDynamic({
    notifications: "checkbox",
    marketing: "checkbox",
  });

  const { notifications, marketing } = formData.getFields();

  const { isValid, errors } = validateFormData(
    switchSchema,
    formData.getValues()
  );
  const formErrors = errors || undefined;

  const handleOnSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    formData.setFetchStatus("loading");

    if (!isValid) {
      formData.setFetchStatus("error");
      formData.touchForm();
      return;
    }

    // Simulate API call
    await new Promise((resolve) => setTimeout(resolve, 1000));
    formData.setFetchStatus("success");
    console.log("Form submitted:", formData.getValues());
  };

  const formDataForAlert = {
    fetchStatus: formData.fetchStatus,
    error: formData.error,
  };

  return (
    <Form
      ref={formRef}
      onSubmit={handleOnSubmit}
      errors={formErrors}
      id="form-switch"
      className="flex flex-col gap-1"
    >
      <FormAlert formData={formDataForAlert} />
      <FormSwitch field={notifications} label="Enable notifications" />
      <FormSwitch field={marketing} label="Receive marketing emails" />
      <FormSubmit
        fetchStatus={formData.fetchStatus}
        buttonProps={{ children: "Submit" }}
        form="form-switch"
      />
    </Form>
  );
};

WithCheckbox

export const WithCheckbox = () => {
  const formRef = useRef<HTMLFormElement>(null);

  const formData = useFormDynamic({
    terms: "checkbox",
    privacy: "checkbox",
  });

  const { terms, privacy } = formData.getFields();

  const { isValid, errors } = validateFormData(
    checkboxSchema,
    formData.getValues()
  );
  const formErrors = errors || undefined;

  const handleOnSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    formData.setFetchStatus("loading");

    if (!isValid) {
      formData.setFetchStatus("error");
      formData.touchForm();
      formData.setError("Please accept all required agreements");
      return;
    }

    // Simulate API call
    await new Promise((resolve) => setTimeout(resolve, 1000));
    formData.setFetchStatus("success");
    console.log("Form submitted:", formData.getValues());
  };

  const formDataForAlert = {
    fetchStatus: formData.fetchStatus,
    error: formData.error,
  };

  return (
    <Form
      ref={formRef}
      onSubmit={handleOnSubmit}
      errors={formErrors}
      id="form-checkbox"
      className="flex flex-col gap-1"
    >
      <FormAlert formData={formDataForAlert} />
      <FormCheckbox
        field={terms}
        label="I agree to the terms and conditions *"
      />
      <FormCheckbox field={privacy} label="I agree to the privacy policy *" />
      <FormSubmit
        fetchStatus={formData.fetchStatus}
        buttonProps={{ children: "Submit" }}
        form="form-checkbox"
      />
    </Form>
  );
};

WithCheckboxGroup

export const WithCheckboxGroup = () => {
  const formRef = useRef<HTMLFormElement>(null);

  const formData = useFormDynamic({
    interests: "checkbox",
  });

  const { interests } = formData.getFields();

  const { isValid, errors } = validateFormData(
    checkboxGroupSchema,
    formData.getValues()
  );
  const formErrors = errors || undefined;

  const interestsOptions = [
    {
      value: "react",
      label: "React",
      description: "A JavaScript library for building user interfaces",
    },
    {
      value: "typescript",
      label: "TypeScript",
      description: "JavaScript with syntax for types",
    },
    {
      value: "tailwind",
      label: "Tailwind CSS",
      description: "A utility-first CSS framework",
    },
    {
      value: "nextjs",
      label: "Next.js",
      description: "The React Framework for the Web",
    },
  ];

  const handleOnSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    formData.setFetchStatus("loading");

    if (!isValid) {
      formData.setFetchStatus("error");
      formData.touchForm();
      formData.setError("Please select at least one interest");
      return;
    }

    // Simulate API call
    await new Promise((resolve) => setTimeout(resolve, 1000));
    formData.setFetchStatus("success");
    console.log("Form submitted:", formData.getValues());
  };

  const formDataForAlert = {
    fetchStatus: formData.fetchStatus,
    error: formData.error,
  };

  return (
    <Form
      ref={formRef}
      onSubmit={handleOnSubmit}
      errors={formErrors}
      id="form-checkbox-group"
      className="flex flex-col gap-1"
    >
      <FormAlert formData={formDataForAlert} />
      <FormCheckboxGroup
        field={interests}
        label="Select your interests *"
        options={interestsOptions}
      />
      <FormSubmit
        fetchStatus={formData.fetchStatus}
        buttonProps={{ children: "Submit" }}
        form="form-checkbox-group"
      />
    </Form>
  );
};

WithSelect

export const WithSelect = () => {
  const formRef = useRef<HTMLFormElement>(null);

  const formData = useFormDynamic({
    country: "text",
    city: "text",
  });

  const { country, city } = formData.getFields();

  const { isValid, errors } = validateFormData(
    selectSchema,
    formData.getValues()
  );
  const formErrors = errors || undefined;

  const countryOptions = [
    { label: "United States", value: "us" },
    { label: "United Kingdom", value: "uk" },
    { label: "Canada", value: "ca" },
    { label: "Australia", value: "au" },
  ];

  const cityOptions = [
    { label: "New York", value: "ny" },
    { label: "Los Angeles", value: "la" },
    { label: "Chicago", value: "ch" },
    { label: "San Francisco", value: "sf" },
  ];

  const handleOnSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    formData.setFetchStatus("loading");

    if (!isValid) {
      formData.setFetchStatus("error");
      formData.touchForm();
      formData.setError("Please select both country and city");
      return;
    }

    // Simulate API call
    await new Promise((resolve) => setTimeout(resolve, 1000));
    formData.setFetchStatus("success");
    console.log("Form submitted:", formData.getValues());
  };

  const formDataForAlert = {
    fetchStatus: formData.fetchStatus,
    error: formData.error,
  };

  return (
    <Form
      ref={formRef}
      onSubmit={handleOnSubmit}
      errors={formErrors}
      id="form-select"
      className="flex flex-col gap-1"
    >
      <FormAlert formData={formDataForAlert} />
      <FormFieldSelect
        field={country}
        label="Country *"
        options={countryOptions}
        placeholder="Select a country"
      />
      <FormFieldSelect
        field={city}
        label="City *"
        options={cityOptions}
        placeholder="Select a city"
      />
      <FormSubmit
        fetchStatus={formData.fetchStatus}
        buttonProps={{ children: "Submit" }}
        form="form-select"
      />
    </Form>
  );
};

WithLoading

export const WithLoading = () => {
  const formRef = useRef<HTMLFormElement>(null);

  const formData = useFormDynamic({
    name: "text",
    email: "text",
  });

  const { name, email } = formData.getFields();

  const { isValid, errors } = validateFormData(
    defaultSchema,
    formData.getValues()
  );
  const formErrors = errors || undefined;

  const handleOnSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    formData.setFetchStatus("loading");

    if (!isValid) {
      formData.setFetchStatus("error");
      formData.touchForm();
      formData.setError("Please complete all required fields");
      return;
    }

    // Simulate API call
    await new Promise((resolve) => setTimeout(resolve, 2000));
    formData.setFetchStatus("success");
    console.log("Form submitted:", formData.getValues());
  };

  const formDataForAlert = {
    fetchStatus: formData.fetchStatus,
    error: formData.error,
  };

  return (
    <Form
      ref={formRef}
      onSubmit={handleOnSubmit}
      errors={formErrors}
      id="form-loading"
      className="flex flex-col gap-1"
    >
      <FormAlert formData={formDataForAlert} />
      <FormField field={name} label="Name *">
        <Input type="text" placeholder="Enter your name" />
      </FormField>
      <FormField field={email} label="Email *">
        <Input type="email" placeholder="Enter your email" />
      </FormField>
      <FormSubmit
        fetchStatus={formData.fetchStatus}
        buttonProps={{ children: "Submit" }}
        form="form-loading"
      />
    </Form>
  );
};

CompleteForm

export const CompleteForm = () => {
  const formRef = useRef<HTMLFormElement>(null);

  const formData = useFormDynamic({
    name: "text",
    email: "text",
    phone: "text",
    message: "textarea",
    country: "select",
    notifications: "checkbox",
    terms: "checkbox",
    interests: "checkbox",
  });

  const {
    name,
    email,
    phone,
    message,
    country,
    notifications,
    terms,
    interests,
  } = formData.getFields();

  const { isValid, errors } = validateFormData(
    completeFormSchema,
    formData.getValues()
  );
  const formErrors = errors || undefined;

  const interestsOptions = [
    {
      value: "web",
      label: "Web Development",
      description: "Building websites and web applications",
    },
    {
      value: "mobile",
      label: "Mobile Development",
      description: "Creating mobile apps",
    },
    {
      value: "design",
      label: "UI/UX Design",
      description: "Designing user interfaces",
    },
  ];

  const handleOnSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    formData.setFetchStatus("loading");

    if (!isValid) {
      formData.setFetchStatus("error");
      formData.touchForm();
      formData.setError("Please complete all required fields");
      return;
    }

    // Simulate API call
    await new Promise((resolve) => setTimeout(resolve, 1000));
    formData.setFetchStatus("success");
    console.log("Form submitted:", formData.getValues());
  };

  const formDataForAlert = {
    fetchStatus: formData.fetchStatus,
    error: formData.error,
  };

  return (
    <Form
      ref={formRef}
      onSubmit={handleOnSubmit}
      errors={formErrors}
      id="form-complete"
      className="flex flex-col gap-1 smash"
    >
      <FormAlert formData={formDataForAlert} />
      <FormField field={name} label="Full Name *">
        <Input type="text" placeholder="Enter your full name" />
      </FormField>
      <FormField field={email} label="Email Address *">
        <Input type="email" placeholder="Enter your email" />
      </FormField>
      <FormField field={phone} label="Phone Number">
        <Input type="tel" placeholder="Enter your phone number" />
      </FormField>
      <FormField field={country} label="Country">
        <select>
          <option value="">Select a country</option>
          <option value="us">United States</option>
          <option value="uk">United Kingdom</option>
          <option value="ca">Canada</option>
          <option value="au">Australia</option>
        </select>
      </FormField>
      <FormField field={message} label="Message">
        <textarea placeholder="Enter your message" rows={4} />
      </FormField>
      <FormSwitch field={notifications} label="Enable email notifications" />
      <FormCheckboxGroup
        field={interests}
        label="Select your interests *"
        options={interestsOptions}
        className="bg-gray-1 p-2 border border-gray-a6 rounded-sm my-1"
      />
      <FormCheckbox
        field={terms}
        label="I agree to the terms and conditions *"
      />
      
      <FormSubmit
        fetchStatus={formData.fetchStatus}
        buttonProps={{ children: "Submit Form" }}
        form="form-complete"
      />
    </Form>
  );
};

WithComboboxSingle

export const WithComboboxSingle = () => {
  const formRef = useRef<HTMLFormElement>(null);

  const formData = useFormDynamic({
    country: "text",
  });

  const { country } = formData.getFields();

  const { isValid, errors } = validateFormData(
    comboboxSingleSchema,
    formData.getValues()
  );
  const formErrors = errors || undefined;

  const handleOnSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    formData.setFetchStatus("loading");

    if (!isValid) {
      formData.setFetchStatus("error");
      formData.touchForm();
      formData.setError("Please select a country");
      return;
    }

    // Simulate API call
    await new Promise((resolve) => setTimeout(resolve, 1000));
    formData.setFetchStatus("success");
    console.log("Form submitted:", formData.getValues());
  };

  const formDataForAlert = {
    fetchStatus: formData.fetchStatus,
    error: formData.error,
  };

  return (
    <Form
      ref={formRef}
      onSubmit={handleOnSubmit}
      errors={formErrors}
      id="form-combobox-single"
      className="flex flex-col gap-1"
    >
      <FormAlert formData={formDataForAlert} />
      <FormFieldCombobox
        field={country}
        label="Country *"
        options={comboboxOptions}
        placeholder="Select a country"
      />
      <FormSubmit
        fetchStatus={formData.fetchStatus}
        buttonProps={{ children: "Submit" }}
        form="form-combobox-single"
      />
    </Form>
  );
};

WithComboboxMultiple

export const WithComboboxMultiple = () => {
  const formRef = useRef<HTMLFormElement>(null);

  const formData = useFormDynamic({
    interests: "text",
  }, formRef);

  const { interests } = formData.getFields();

  const { isValid, errors } = validateFormData(
    comboboxMultipleSchema,
    formData.getValues()
  );
  const formErrors = errors || undefined;

  const handleOnSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    formData.setFetchStatus("loading");

    if (!isValid) {
      formData.setFetchStatus("error");
      formData.touchForm();
      formData.setError("Please select at least one interest");
      return;
    }

    // Simulate API call
    await new Promise((resolve) => setTimeout(resolve, 1000));
    formData.setFetchStatus("success");
    console.log("Form submitted:", formData.getValues());
  };

  const formDataForAlert = {
    fetchStatus: formData.fetchStatus,
    error: formData.error,
  };


  return (
    <Form
      ref={formRef}
      onSubmit={handleOnSubmit}
      errors={formErrors}
      id="form-combobox-multiple"
      className="flex flex-col gap-1"
    >
      <FormAlert formData={formDataForAlert} />
      <FormFieldCombobox
        field={interests}
        label="Select your interests *"
        options={comboboxInterestsOptions}
        multiple
        placeholder="Select one or more interests"
      />
      <FormSubmit
        fetchStatus={formData.fetchStatus}
        buttonProps={{ children: "Submit" }}
        form="form-combobox-multiple"
      />
    </Form>
  );
};

WithSignaturePad

export const WithSignaturePad = () => {
  const formRef = useRef<HTMLFormElement>(null);

  const formData = useFormDynamic({
    signature: "text",
  });

  const { signature } = formData.getFields();

  const { isValid, errors } = validateFormData(
    signaturePadSchema,
    formData.getValues()
  );
  const formErrors = errors || undefined;

  const handleOnSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    formData.setFetchStatus("loading");

    if (!isValid) {
      formData.setFetchStatus("error");
      formData.touchForm();
      formData.setError("Please provide your signature");
      return;
    }

    // Simulate API call
    await new Promise((resolve) => setTimeout(resolve, 1000));
    formData.setFetchStatus("success");
    console.log("Form submitted:", formData.getValues());
  };

  const formDataForAlert = {
    fetchStatus: formData.fetchStatus,
    error: formData.error,
  };

  return (
    <Form
      ref={formRef}
      onSubmit={handleOnSubmit}
      errors={formErrors}
      id="form-signature-pad"
      className="flex flex-col gap-1"
    >
      <FormAlert formData={formDataForAlert} />
      <FormFieldSignaturePad
        field={signature}
        label="Signature *"
        variant="default"
        size="md"
      />
      <FormSubmit
        fetchStatus={formData.fetchStatus}
        buttonProps={{ children: "Submit" }}
        form="form-signature-pad"
      />
    </Form>
  );
};

WithSortableList

export const WithSortableList = () => {
  const formRef = useRef<HTMLFormElement>(null);

  const formData = useFormDynamic({
    priorities: "text",
  });

  const { priorities } = formData.getFields();

  // Initialize with array value for sortable list
  React.useEffect(() => {
    if (!Array.isArray(priorities.value)) {
      priorities.setValue(["High Priority", "Medium Priority", "Low Priority"] as any);
    }
  }, []);

  const { isValid, errors } = validateFormData(
    sortableListSchema,
    formData.getValues()
  );
  const formErrors = errors || undefined;

  // Filter errors for the priorities field
  const prioritiesErrors = React.useMemo(() => {
    if (!formErrors) return undefined;
    return formErrors.filter(
      (error) => error.instancePath === `/${priorities.name}`
    );
  }, [formErrors, priorities.name]);

  const handleOnSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    formData.setFetchStatus("loading");

    if (!isValid) {
      formData.setFetchStatus("error");
      formData.touchForm();
      formData.setError("Please ensure at least one priority is added");
      return;
    }

    // Simulate API call
    await new Promise((resolve) => setTimeout(resolve, 1000));
    formData.setFetchStatus("success");
    console.log("Form submitted:", formData.getValues());
  };

  const formDataForAlert = {
    fetchStatus: formData.fetchStatus,
    error: formData.error,
  };

  return (
    <Form
      ref={formRef}
      onSubmit={handleOnSubmit}
      errors={formErrors}
      id="form-sortable-list"
      className="flex flex-col gap-1"
    >
      <FormAlert formData={formDataForAlert} />
      <FormFieldSortableList
        field={priorities}
        label="Priorities *"
        errors={prioritiesErrors}
        renderItem={(item) => (
          <span className="text-gray-12 flex-1">{String(item)}</span>
        )}
      />
      <FormSubmit
        fetchStatus={formData.fetchStatus}
        buttonProps={{ children: "Submit" }}
        form="form-sortable-list"
      />
    </Form>
  );
};

WithEditor

export const WithEditor = () => {
  const formRef = useRef<HTMLFormElement>(null);

  const formData = useFormDynamic({
    content: "text",
  });

  const { content } = formData.getFields();

  console.log(content.value);

  const { isValid, errors } = validateFormData(
    editorSchema,
    formData.getValues()
  );
  const formErrors = errors || undefined;

  const handleOnSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    formData.setFetchStatus("loading");

    if (!isValid) {
      formData.setFetchStatus("error");
      formData.touchForm();
      formData.setError("Please enter some content");
      return;
    }

    // Simulate API call
    await new Promise((resolve) => setTimeout(resolve, 1000));
    formData.setFetchStatus("success");
    console.log("Form submitted:", formData.getValues());
  };

  const formDataForAlert = {
    fetchStatus: formData.fetchStatus,
    error: formData.error,
  };

  return (
    <Form
      ref={formRef}
      onSubmit={handleOnSubmit}
      errors={formErrors}
      id="form-editor"
      className="flex flex-col gap-1"
    >
      <FormAlert formData={formDataForAlert} />
      <FormFieldEditor
        field={content}
        label="Content *"
      />
      <FormSubmit
        fetchStatus={formData.fetchStatus}
        buttonProps={{ children: "Submit" }}
        form="form-editor"
      />
    </Form>
  );
};