Files
monitoring-app/.agents/skills/mantine-dev/references/forms.md
bipproduction 39d659acd0 skills
2026-04-01 10:43:03 +08:00

10 KiB

Forms Reference

@mantine/form provides useForm hook for managing form state, validation, and submission.

Installation

npm install @mantine/form

No styles needed — works with or without @mantine/core.

Basic Usage

import { useForm } from '@mantine/form';
import { TextInput, Button, Box } from '@mantine/core';

interface FormValues {
  email: string;
  name: string;
}

function Demo() {
  const form = useForm<FormValues>({
    mode: 'uncontrolled',  // Recommended for performance
    initialValues: {
      email: '',
      name: '',
    },
    validate: {
      email: (value) => (/^\S+@\S+$/.test(value) ? null : 'Invalid email'),
      name: (value) => (value.length < 2 ? 'Name too short' : null),
    },
  });

  return (
    <Box component="form" onSubmit={form.onSubmit((values) => console.log(values))}>
      <TextInput
        label="Name"
        placeholder="Your name"
        key={form.key('name')}
        {...form.getInputProps('name')}
      />
      <TextInput
        label="Email"
        placeholder="your@email.com"
        key={form.key('email')}
        {...form.getInputProps('email')}
      />
      <Button type="submit" mt="md">Submit</Button>
    </Box>
  );
}

useForm Options

interface UseFormInput<Values> {
  mode?: 'controlled' | 'uncontrolled';  // Default: 'controlled'
  initialValues?: Values;
  initialErrors?: FormErrors;
  initialDirty?: Record<string, boolean>;
  initialTouched?: Record<string, boolean>;
  validate?: FormValidation<Values>;
  validateInputOnChange?: boolean | string[];
  validateInputOnBlur?: boolean | string[];
  clearInputErrorOnChange?: boolean;
  onValuesChange?: (values: Values, previous: Values) => void;
  onSubmitPreventDefault?: 'always' | 'never' | 'validation-failed';
}

Controlled vs Uncontrolled Mode

Better performance — values stored in DOM:

const form = useForm({
  mode: 'uncontrolled',  // Add mode
  initialValues: { name: '' },
});

<TextInput
  key={form.key('name')}  // Required for uncontrolled
  {...form.getInputProps('name')}
/>

Controlled

Values stored in React state — re-renders on every change:

const form = useForm({
  mode: 'controlled',
  initialValues: { name: '' },
});

<TextInput {...form.getInputProps('name')} />

Form Values

// Get all values
const values = form.getValues();

// Set single field
form.setFieldValue('email', 'new@email.com');

// Set multiple values
form.setValues({ name: 'John', email: 'john@email.com' });

// Set values from previous state
form.setValues((prev) => ({ ...prev, name: 'Updated' }));

// Reset to initialValues
form.reset();

// Reset single field
form.resetField('email');

// Update initialValues (affects reset)
form.setInitialValues({ name: 'New Initial' });

Validation

Inline Rules

const form = useForm({
  mode: 'uncontrolled',
  initialValues: {
    email: '',
    age: 0,
    password: '',
    confirmPassword: '',
  },
  validate: {
    email: (value) => (/^\S+@\S+$/.test(value) ? null : 'Invalid email'),
    age: (value) => (value < 18 ? 'Must be 18+' : null),
    // Access all values for cross-field validation
    confirmPassword: (value, values) =>
      value !== values.password ? 'Passwords do not match' : null,
  },
});

Function-based Validation

const form = useForm({
  mode: 'uncontrolled',
  initialValues: { name: '', email: '' },
  validate: (values) => ({
    name: values.name.length < 2 ? 'Name too short' : null,
    email: !values.email.includes('@') ? 'Invalid email' : null,
  }),
});

Schema Validation (Zod, Yup, Joi)

import { zodResolver } from 'mantine-form-zod-resolver';
import { z } from 'zod';

const schema = z.object({
  name: z.string().min(2, 'Name must have at least 2 characters'),
  email: z.string().email('Invalid email'),
  age: z.number().min(18, 'Must be 18 or older'),
});

const form = useForm({
  mode: 'uncontrolled',
  initialValues: { name: '', email: '', age: 0 },
  validate: zodResolver(schema),
});

Install resolver:

npm install mantine-form-zod-resolver zod
# or
npm install mantine-form-yup-resolver yup
# or
npm install mantine-form-joi-resolver joi

Validation Timing

const form = useForm({
  mode: 'uncontrolled',
  validateInputOnChange: true,   // Validate all on change
  validateInputOnBlur: true,     // Validate all on blur
  clearInputErrorOnChange: true, // Clear error when value changes (default)
});

// Validate specific fields only
const form = useForm({
  mode: 'uncontrolled',
  validateInputOnChange: ['email', 'password'],
  validateInputOnBlur: ['email'],
});

Manual Validation

// Validate all fields
const result = form.validate();
// result.hasErrors: boolean
// result.errors: FormErrors

// Validate single field
form.validateField('email');

// Check if valid (without setting errors)
const isValid = form.isValid();
const isEmailValid = form.isValid('email');

Errors

// Get current errors
form.errors; // { email: 'Invalid', name: null }

// Set error
form.setFieldError('email', 'This email is taken');

// Set multiple errors
form.setErrors({ email: 'Invalid', name: 'Required' });

// Clear all errors
form.clearErrors();

// Clear single field error
form.clearFieldError('email');

Form Submission

<form
  onSubmit={form.onSubmit(
    // Success handler - called when validation passes
    (values, event) => {
      console.log('Valid:', values);
      // Submit to API
    },
    // Error handler - called when validation fails
    (errors, values, event) => {
      console.log('Errors:', errors);
      // Show notification, focus first error, etc.
    }
  )}
>
  {/* inputs */}
</form>

Nested Objects

const form = useForm({
  mode: 'uncontrolled',
  initialValues: {
    user: {
      firstName: '',
      lastName: '',
      address: {
        city: '',
        country: '',
      },
    },
  },
  validate: {
    user: {
      firstName: (value) => (value.length < 2 ? 'Too short' : null),
      address: {
        city: (value) => (!value ? 'Required' : null),
      },
    },
  },
});

<TextInput
  key={form.key('user.firstName')}
  {...form.getInputProps('user.firstName')}
/>
<TextInput
  key={form.key('user.address.city')}
  {...form.getInputProps('user.address.city')}
/>

List Fields

const form = useForm({
  mode: 'uncontrolled',
  initialValues: {
    employees: [
      { name: '', email: '' },
    ],
  },
});

// Add item
form.insertListItem('employees', { name: '', email: '' });

// Add at specific index
form.insertListItem('employees', { name: '', email: '' }, 0);

// Remove item
form.removeListItem('employees', 1);

// Replace item
form.replaceListItem('employees', 0, { name: 'New', email: 'new@email.com' });

// Reorder items
form.reorderListItem('employees', { from: 0, to: 2 });

Rendering List

import { FORM_INDEX } from '@mantine/form';

function Demo() {
  const fields = form.getValues().employees.map((item, index) => (
    <Group key={item.key}>
      <TextInput
        key={form.key(`employees.${index}.name`)}
        {...form.getInputProps(`employees.${index}.name`)}
      />
      <TextInput
        key={form.key(`employees.${index}.email`)}
        {...form.getInputProps(`employees.${index}.email`)}
      />
      <ActionIcon onClick={() => form.removeListItem('employees', index)}>
        <IconTrash />
      </ActionIcon>
    </Group>
  ));

  return (
    <Box>
      {fields}
      <Button onClick={() => form.insertListItem('employees', { name: '', email: '' })}>
        Add Employee
      </Button>
    </Box>
  );
}

// Validation for list items with FORM_INDEX
const form = useForm({
  mode: 'uncontrolled',
  validateInputOnChange: [`employees.${FORM_INDEX}.name`],
});

Touched & Dirty State

// Check if any field was interacted with
form.isTouched();
form.isTouched('email');

// Check if values differ from initialValues
form.isDirty();
form.isDirty('email');

// Set touched state
form.setTouched({ email: true, name: false });
form.resetTouched();

// Set dirty state
form.setDirty({ email: true });
form.resetDirty(); // Snapshot current values as "clean"

Form Context

Share form across components without prop drilling:

// formContext.ts
import { createFormContext } from '@mantine/form';

interface FormValues {
  name: string;
  email: string;
}

export const [FormProvider, useFormContext, useForm] = createFormContext<FormValues>();
// Parent component
import { FormProvider, useForm } from './formContext';

function Parent() {
  const form = useForm({
    mode: 'uncontrolled',
    initialValues: { name: '', email: '' },
  });

  return (
    <FormProvider form={form}>
      <NameInput />
      <EmailInput />
    </FormProvider>
  );
}

// Child component
import { useFormContext } from './formContext';

function NameInput() {
  const form = useFormContext();
  return (
    <TextInput
      key={form.key('name')}
      {...form.getInputProps('name')}
    />
  );
}

UseFormReturnType

Type for passing form as prop:

import { UseFormReturnType } from '@mantine/form';

interface Props {
  form: UseFormReturnType<{ name: string; email: string }>;
}

function NameField({ form }: Props) {
  return (
    <TextInput
      key={form.key('name')}
      {...form.getInputProps('name')}
    />
  );
}

Built-in Validators

import { isNotEmpty, isEmail, hasLength, matches, isInRange } from '@mantine/form';

const form = useForm({
  mode: 'uncontrolled',
  initialValues: {
    name: '',
    email: '',
    age: 0,
    website: '',
    terms: false,
  },
  validate: {
    name: isNotEmpty('Name is required'),
    email: isEmail('Invalid email'),
    age: isInRange({ min: 18, max: 99 }, 'Age must be 18-99'),
    website: matches(/^https?:\/\//, 'Must start with http'),
    terms: isNotEmpty('Must accept terms'),
  },
});

Focus First Error

<form
  onSubmit={form.onSubmit(
    (values) => { /* success */ },
    (errors) => {
      const firstErrorPath = Object.keys(errors)[0];
      form.getInputNode(firstErrorPath)?.focus();
    }
  )}
>