Skip to main content

Forms & Validation

DEVELOPER Intermediate

Create forms with validation using Ant Design Form components.

Overview

EZ-Console uses Ant Design's Form component for form management and validation. This guide covers creating forms, validation rules, and handling form submissions.

Basic Form

Simple Form

import { Form, Input, Button } from 'antd';
import { useTranslation } from 'ez-console';

export const ProductForm: React.FC = () => {
const { t } = useTranslation();
const [form] = Form.useForm();

const handleSubmit = (values: any) => {
console.log('Form values:', values);
};

return (
<Form form={form} onFinish={handleSubmit} layout="vertical">
<Form.Item
name="name"
label={t('product.name')}
rules={[{ required: true, message: t('product.nameRequired') }]}
>
<Input placeholder={t('product.namePlaceholder')} />
</Form.Item>

<Form.Item>
<Button type="primary" htmlType="submit">
{t('common.submit')}
</Button>
</Form.Item>
</Form>
);
};

Validation Rules

Built-in Rules

<Form.Item
name="email"
label="Email"
rules={[
{ required: true, message: 'Email is required' },
{ type: 'email', message: 'Invalid email format' },
]}
>
<Input />
</Form.Item>

<Form.Item
name="age"
label="Age"
rules={[
{ required: true, message: 'Age is required' },
{ type: 'number', min: 0, max: 120, message: 'Age must be between 0 and 120' },
]}
>
<InputNumber />
</Form.Item>

Custom Validation

<Form.Item
name="password"
label="Password"
rules={[
{ required: true, message: 'Password is required' },
{
validator: (_, value) => {
if (!value) {
return Promise.resolve();
}
if (value.length < 8) {
return Promise.reject(new Error('Password must be at least 8 characters'));
}
if (!/[A-Z]/.test(value)) {
return Promise.reject(new Error('Password must contain uppercase letter'));
}
if (!/[0-9]/.test(value)) {
return Promise.reject(new Error('Password must contain a number'));
}
return Promise.resolve();
},
},
]}
>
<Input.Password />
</Form.Item>

Async Validation

<Form.Item
name="username"
label="Username"
rules={[
{ required: true, message: 'Username is required' },
{
validator: async (_, value) => {
if (!value) {
return Promise.resolve();
}
const exists = await checkUsernameExists(value);
if (exists) {
return Promise.reject(new Error('Username already exists'));
}
return Promise.resolve();
},
},
]}
>
<Input />
</Form.Item>

Form Layouts

Vertical Layout (Default)

<Form layout="vertical">
<Form.Item name="name" label="Name">
<Input />
</Form.Item>
</Form>

Horizontal Layout

<Form layout="horizontal" labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
<Form.Item name="name" label="Name">
<Input />
</Form.Item>
</Form>

Inline Layout

<Form layout="inline">
<Form.Item name="search">
<Input placeholder="Search" />
</Form.Item>
<Form.Item>
<Button type="primary">Search</Button>
</Form.Item>
</Form>

Form Controls

Input

<Form.Item name="name" label="Name">
<Input placeholder="Enter name" />
</Form.Item>

<Form.Item name="description" label="Description">
<Input.TextArea rows={4} placeholder="Enter description" />
</Form.Item>

Select

<Form.Item name="category" label="Category">
<Select placeholder="Select category">
<Option value="electronics">Electronics</Option>
<Option value="clothing">Clothing</Option>
<Option value="food">Food</Option>
</Select>
</Form.Item>

DatePicker

import { DatePicker } from 'antd';
import dayjs from 'dayjs';

<Form.Item name="date" label="Date">
<DatePicker format="YYYY-MM-DD" />
</Form.Item>

Upload

import { Upload } from 'antd';
import { UploadOutlined } from '@ant-design/icons';

<Form.Item name="file" label="File">
<Upload>
<Button icon={<UploadOutlined />}>Upload</Button>
</Upload>
</Form.Item>

Form Methods

Programmatic Control

const [form] = Form.useForm();

// Set field values
form.setFieldsValue({
name: 'Product Name',
price: 99.99,
});

// Get field values
const values = form.getFieldsValue();

// Validate form
form.validateFields().then(values => {
console.log('Valid values:', values);
});

// Reset form
form.resetFields();

// Reset specific fields
form.resetFields(['name', 'price']);

Conditional Fields

const [showAdvanced, setShowAdvanced] = useState(false);

<Form.Item name="type" label="Type">
<Select onChange={(value) => setShowAdvanced(value === 'advanced')}>
<Option value="basic">Basic</Option>
<Option value="advanced">Advanced</Option>
</Select>
</Form.Item>

{showAdvanced && (
<Form.Item name="advancedOption" label="Advanced Option">
<Input />
</Form.Item>
)}

Form Submission

Basic Submission

const handleSubmit = (values: any) => {
console.log('Form values:', values);
// Submit to API
};

With API Call

import { useRequest } from 'ahooks';
import { apiPost } from 'ez-console';
import { message } from 'antd';

export const ProductForm: React.FC = () => {
const [form] = Form.useForm();
const navigate = useNavigate();

const { run: submitForm, loading } = useRequest(
(values: Product) => apiPost('/products', values),
{
manual: true,
onSuccess: () => {
message.success('Product created successfully');
navigate('/products');
},
onError: (error) => {
message.error(error.message || 'Failed to create product');
},
}
);

const handleSubmit = (values: any) => {
submitForm(values);
};

return (
<Form form={form} onFinish={handleSubmit}>
{/* Form fields */}
<Form.Item>
<Button type="primary" htmlType="submit" loading={loading}>
Submit
</Button>
</Form.Item>
</Form>
);
};

Edit Form

import { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useRequest } from 'ahooks';
import { apiGet, apiPut } from 'ez-console';

export const ProductEditForm: React.FC = () => {
const { id } = useParams<{ id: string }>();
const [form] = Form.useForm();

// Fetch product data
const { data: product, loading: loadingProduct } = useRequest(
() => apiGet(`/products/${id}`),
{
ready: !!id,
onSuccess: (data) => {
form.setFieldsValue(data);
},
}
);

// Update product
const { run: updateProduct, loading: updating } = useRequest(
(values: Product) => apiPut(`/products/${id}`, values),
{
manual: true,
onSuccess: () => {
message.success('Product updated successfully');
},
}
);

return (
<Form form={form} onFinish={updateProduct}>
{/* Form fields */}
</Form>
);
};

Dynamic Forms

Dynamic Fields

import { Form, Button, Space } from 'antd';
import { PlusOutlined, MinusCircleOutlined } from '@ant-design/icons';

<Form.List name="items">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<Space key={key} style={{ display: 'flex', marginBottom: 8 }} align="baseline">
<Form.Item
{...restField}
name={[name, 'name']}
rules={[{ required: true, message: 'Missing name' }]}
>
<Input placeholder="Item name" />
</Form.Item>
<Form.Item
{...restField}
name={[name, 'quantity']}
rules={[{ required: true, message: 'Missing quantity' }]}
>
<InputNumber placeholder="Quantity" />
</Form.Item>
<MinusCircleOutlined onClick={() => remove(name)} />
</Space>
))}
<Form.Item>
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
Add item
</Button>
</Form.Item>
</>
)}
</Form.List>

Form Validation Messages

Custom Messages

<Form.Item
name="email"
label="Email"
rules={[
{
required: true,
message: 'Please enter your email address',
},
{
type: 'email',
message: 'Please enter a valid email address',
},
]}
>
<Input />
</Form.Item>

Internationalized Messages

import { useTranslation } from 'ez-console';

export const ProductForm: React.FC = () => {
const { t } = useTranslation();

return (
<Form>
<Form.Item
name="name"
label={t('product.name')}
rules={[
{ required: true, message: t('product.nameRequired') },
{ min: 3, message: t('product.nameMinLength') },
]}
>
<Input />
</Form.Item>
</Form>
);
};

Best Practices

1. Use Form Instance

// ✅ Good: Use form instance
const [form] = Form.useForm();
<Form form={form}>

// ❌ Bad: No form instance
<Form>

2. Validate on Submit

// ✅ Good: Validate on submit
<Form onFinish={handleSubmit}>

// ❌ Bad: Validate on change (can be annoying)
<Form onValuesChange={handleValidate}>

3. Clear Error Messages

// ✅ Good: Clear, specific messages
{ required: true, message: 'Name is required' }

// ❌ Bad: Generic messages
{ required: true, message: 'Error' }

4. Use TypeScript

// ✅ Good: Typed form values
interface ProductFormData {
name: string;
price: number;
}

const handleSubmit = (values: ProductFormData) => {
// Type-safe
};

Need help? Ask in GitHub Discussions.