

<Preview name="ValidationFormExample" />

## Zod Integration [#zod-integration]

### Basic Schema Validation (onDynamic) [#basic-schema-validation-ondynamic]

The simplest way to add validation is to define a Zod schema and pass it to `validators.onDynamic` with `revalidateLogic` to control timing:

```tsx
import { hasErrors } from "@tilt-legal/cubitt-components/primitives";
import {
  z } from "zod";

const schema = z.object({
  email: z.string().email("Please enter a valid email"),
  age: z.number().min(18,
  "You must be at least 18 years old"),
  password: z.string().min(8,
  "Password must be at least 8 characters"),
  });

const form = useForm({
  validationLogic: revalidateLogic({
    mode: "submit",
  modeAfterSubmission: "change",
  }),
  validators: {
    onDynamic: schema,
  },
  // ...
});
```

### Cross-Field Validation [#cross-field-validation]

Use Zod's `refine` method for validation that depends on multiple fields:

```tsx
const schema = z
  .object({
    password: z.string().min(8),
  confirmPassword: z.string(),
  })
  .refine((data) => data.password === data.confirmPassword,
  {
    message: "Passwords don't match",
  path: ["confirmPassword"],
  // Error appears on confirmPassword field
  });
```

### Complex Validation Rules [#complex-validation-rules]

Zod supports sophisticated validation patterns:

```tsx
const userSchema = z.object({
  username: z
    .string()
    .min(3,
  "Username must be at least 3 characters")
    .max(20,
  "Username must be less than 20 characters")
    .regex(/^[a-zA-Z0-9_]+$/,
  "Only letters,
  numbers,
  and underscores allowed"),
  email: z
    .string()
    .email("Invalid email address")
    .refine((email) => !email.endsWith("@temp.com"),
  {
      message: "Temporary email addresses are not allowed",
  }),
  age: z.coerce
    .number()
    .int("Age must be a whole number")
    .min(13,
  "You must be at least 13 years old")
    .max(120,
  "Please enter a valid age"),
  website: z
    .string()
    .url("Please enter a valid URL")
    .optional()
    .or(z.literal("")),
  // Allow empty string
});
```

## Custom Validation [#custom-validation]

### Client-Side Custom Validation [#client-side-custom-validation]

For validation logic that can't be expressed with Zod,
use custom validators:

```tsx
const form = useForm({
  validators: {
    onChange: ({ value }) => {
      const errors: Record<string,
  string> = {};

      // Custom validation logic
      if (value.username && value.username.includes("admin")) {
        errors.username = "Username cannot contain 'admin'";
      }

      if (value.password !== value.confirmPassword) {
        errors.confirmPassword = "Passwords must match";
      }

      // Return errors object or undefined if no errors
      return Object.keys(errors).length > 0 ? { fields: errors } : undefined;
    },
  },
  });
```

### Combining Zod and Custom Validation [#combining-zod-and-custom-validation]

You can use both Zod and custom validation together with onDynamic channels:

```tsx
const form = useForm({
  validationLogic: revalidateLogic({
    mode: "submit",
  modeAfterSubmission: "change",
  }),
  validators: {
    onDynamic: zodSchema,
  onDynamicAsyncDebounceMs: 500,
  onDynamicAsync: async ({ value }) => customAsyncValidation(value),
  },
  });
```

## Async Validation [#async-validation]

Form with async validation for checking username availability or other server-side checks.

<Tabs
  items="['Preview',
'Code']"
>
  <Tab value="Preview">
    <Preview name="AsyncValidationExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    const checkUsernameAvailability = async (username: string) => {
      // Simulate API call
      await new Promise((resolve) => setTimeout(resolve,
      500));
      const taken = ["admin",
      "user",
      "test"].includes(username.toLowerCase());
      return !taken;
    };

    const asyncSchema = z.object({
      username: z.string().min(3,
      "Username must be at least 3 characters"),
      email: z.string().email("Invalid email address"),
      });

    export function AsyncValidationExample() {
      const form = useForm({
        defaultValues: {
          username: "",
      email: "",
      },
      validationLogic: revalidateLogic({
          mode: "submit",
      modeAfterSubmission: "change",
      }),
      validators: {
          onDynamic: asyncSchema,
      onDynamicAsyncDebounceMs: 500,
      onDynamicAsync: async ({ value }) => {
            if (value.username && value.username.length >= 3) {
              const available = await checkUsernameAvailability(value.username);
              if (!available) {
                return { fields: { username: "Username is already taken" } };
              }
            }
            return undefined;
          },
      },
      onSubmit: async ({ value }) => {
          console.log("Submitted:",
      value);
          alert("Username is available!");
        },
      });

      const isValidating = useStore(
        form.store,
      (s) => s.fieldMeta.username?.isValidating ?? false
      );

      return (
        <Form form={form} className="space-y-4 max-w-md">
          <InputField
            name="username"
            label="Username"
            placeholder="Choose a unique username"
            disabled={isValidating}
          />

          <InputField
            name="email"
            label="Email"
            type="email"
            placeholder="your@email.com"
          />

          <Button type="submit">Check Availability</Button>
        </Form>
      );
    }
    ```
  </Tab>
</Tabs>

### Async Validation Patterns [#async-validation-patterns]

#### Debounced Validation [#debounced-validation]

Prevent excessive API calls by debouncing async validation:

```tsx
const form = useForm({
  validators: {
    onDynamicAsyncDebounceMs: 500,
  onDynamicAsync: async ({ value }) => {
      if (value.username) {
        const isAvailable = await checkUsername(value.username);
        if (!isAvailable) return { fields: { username: "Username taken" } };
      }
    },
  },
  });
```

#### Conditional Async Validation [#conditional-async-validation]

Only run async validation when certain conditions are met:

```tsx
validators: {
  onDynamicAsync: async ({ value }) => {
    // Only check username if it meets basic requirements
    if (value.username && value.username.length >= 3) {
      const result = await validateUsername(value.username);
      if (!result.isValid) {
        return { fields: { username: result.message } };
      }
    }
  },
  }
```

#### Multiple Async Checks [#multiple-async-checks]

Validate multiple fields asynchronously:

```tsx
validators: {
  onDynamicAsync: async ({ value }) => {
    const errors: Record<string,
  string> = {};

    // Check username availability
    if (value.username && value.username.length >= 3) {
      const usernameAvailable = await checkUsername(value.username);
      if (!usernameAvailable) {
        errors.username = "Username is taken";
      }
    }

    // Check email availability
    if (value.email && value.email.includes('@')) {
      const emailAvailable = await checkEmail(value.email);
      if (!emailAvailable) {
        errors.email = "Email is already registered";
      }
    }

    return Object.keys(errors).length > 0 ? { fields: errors } : undefined;
  },
  }
```

## Error Handling [#error-handling]

### Form Submission Errors [#form-submission-errors]

Handle errors that occur during form submission:

```tsx
const form = useForm({
  onSubmit: async ({ value,
  formApi }) => {
    try {
      await submitUserData(value);
      alert("Success! Account created.");
    } catch (error) {
      if (error instanceof ValidationError) {
        // Server returned validation errors - use updater function
        formApi.setFieldMeta("username",
  (prev) => ({
          ...prev,
  errorMap: { onSubmit: error.message },
  }));
      } else {
        // Generic error handling
        console.error("Submission failed:",
  error);
        alert("Something went wrong. Please try again.");
      }
    }
  },
  });
```

### Field-Level Error Display [#field-level-error-display]

Access and display field-specific errors:

```tsx
// Using the clean API - errors are handled automatically
<InputField name="username" label="Username" />

// Or access errors manually in advanced scenarios
<form.Field name="username">
  {(field) => {
    const hasError = field.state.meta.errors.length > 0;
    return (
      <div>
        <InputField field={field} label="Username" />
        {hasError && (
          <p className="text-red-500 text-sm mt-1">
            {field.state.meta.errors[0]}
          </p>
        )}
      </div>
    );
  }}
</form.Field>
```

### Server-Side Validation [#server-side-validation]

Handle validation errors returned from the server:

```tsx
const form = useForm({
  onSubmit: async ({ value,
  formApi }) => {
    try {
      await createAccount(value);
    } catch (error) {
      if (error.status === 422) {
        // Server returned field-specific validation errors
        const serverErrors = error.response.data.errors;

        // Map server errors to form fields using the updater pattern
        for (const [fieldName,
  message] of Object.entries(serverErrors)) {
          formApi.setFieldMeta(fieldName,
  (prev) => ({
            ...prev,
  errorMap: { onSubmit: message },
  }));
        }
      }
    }
  },
  });
```

## Validation Timing [#validation-timing]

### onChange vs onSubmit [#onchange-vs-onsubmit]

Choose when validation runs based on your UX requirements:

```tsx
const form = useForm({
  validators: {
    // Real-time validation as user types
    onChange: schema,
  // Only validate when user submits
    onSubmit: schema,
  // Async validation (always debounced)
    onChangeAsync: asyncValidator,
  },
  });
```

### Field-Level Validation Timing [#field-level-validation-timing]

Control validation timing for individual fields:

```tsx
<form.Field
  name="password"
  validators={{
    onChange: ({ value }) =>
      value.length < 8 ? "Password too short" : undefined,
  onBlur: ({ value }) => validatePasswordStrength(value),
  }}
>
  {(field) => <InputField field={field} />}
</form.Field>
```

## Helper Utilities [#helper-utilities]

Cubitt provides type-safe helper functions for common form operations:

### Error Extraction [#error-extraction]

```tsx
import { extractErrorMessages } from "@tilt-legal/cubitt-components/primitives";

// Extract human-readable error messages from field meta
const errors = extractErrorMessages(field.state.meta.errors);
// ["Name is required", "Must be at least 3 characters"]

// Check if field has any validation errors
if (hasErrors(field.state.meta.errors)) {
  // Show error styling
}
```

### Form Helpers [#form-helpers]

For advanced scenarios where you need to programmatically manipulate form state:

```tsx
import {
  setFieldMeta,
  setFieldErrors,
  setFieldValue,
  getFieldValue,
  validateForm,
  validateField,
} from "@tilt-legal/cubitt-components/primitives";

// Set custom errors on a field
setFieldErrors(form, "email", ["Email is already registered"]);

// Programmatically set a field value
setFieldValue(form, "status", "approved");

// Trigger form-wide validation
validateForm(form);

// Validate a specific field
validateField(form, "email", "blur");
```

<Callout type="info">
  These utilities are primarily used internally by the FormBuilder components.
  For most use cases, the standard TanStack Form API is sufficient.
</Callout>
