10 KiB
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
Uncontrolled (Recommended)
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();
}
)}
>