Trong quá trình phát triển các ứng dụng web với ReactJs, việc xây dựng form là một tác vụ không thể tránh khỏi. Tuy nhiên, khi các form trở nên phức tạp với hàng chục field nhập liệu, việc "hardcode" từng <label> và <input> không chỉ tốn thời gian mà còn dẫn đến code lặp lại, khó đọc và khó bảo trì.
Bài viết này sẽ hướng dẫn bạn cách xây dựng một Reusable Form Component, bằng cách tạo ra 1 form, nhưng tách nhiều thành phần trong form ra thành từng component khác nhau.
Cấu trúc Project:
src/
├── types/ # Thư mục chứa các định nghĩa kiểu dữ liệu (TypeScript interfaces/types)
│ └── formFields.ts
├── components/ # Thư mục chứa các UI component
│ └── DynamicForm.tsx # Component chính của form động
└── App.tsx
Ví dụ
Trong ví dụ này, mình xây dựng 1 form gồm 2 input là name với gender, với 1 form chính DynamicForm.tsx và 1 form Component (chứa 2 input). Submit button sẽ nằm ở trang Dynamic FormĐịnh nghĩa Form fields
Định nghĩa cấu trúc field
Chúng ta sẽ định nghĩa các loại input, các property của HTML ở đây// src/types/formFields.ts
// Định nghĩa các loại input mà form của chúng ta hỗ trợ
export type InputType = 'text' | 'number' | 'select';
export interface FormField {
label: string;
name: string;
type: InputType;
placeholder: string;
options?: string[];
}
Định nghĩa kiểu dữ liệu của Form
Định nghĩa cấu trúc dữ liệu tổng thể mà form sẽ trả về sau khi submitexport interface SimpleFormData {
name: string;
gender: string;
}
Component form chính
Đây là component React sẽ đọc định nghĩa từ formFields.ts và dùng phương thức map() để tự động render các trường nhập liệu tương ứng. Chúng ta sẽ kết hợp với React Hook Form để xử lý trạng thái form, validation và submit một cách hiệu quả.
Mình sẽ đi từng bước để integrate dễ dàng hơn
Đầu tiên bạn khai báo DyanmicFormimport type { FormField } from '../../types/formFields';
import type { SimpleFormData } from './SimpleFormData';
const DynamicForm = () => {
// Logic của component sẽ được thêm vào đây
return (
<form>
{/* Giao diện form sẽ được render tại đây */}
<button type="submit">Submit</button>
</form>
);
};
export default DynamicForm;
Khai báo react-hook-form trong DynamicForm.tsx
import { useForm, type SubmitHandler } from 'react-hook-form';
import type { FormField } from '../../types/formFields';
import type { SimpleFormData } from './SimpleFormData';
const DynamicForm = () => {
const { register, handleSubmit, formState: { errors } } = useForm<SimpleFormData>();
return (
<form>
{/* Giao diện form sẽ được render tại đây */}
<button type="submit">Submit</button>
</form>
);
};
export default DynamicForm;
Định nghĩa Dữ liệu Form
fields: FormField[] = [...]: Đây là mảng FormField mà bạn đã định nghĩa. Nó là nguồn dữ liệu để component của chúng ta tự động tạo ra các input trong form.const fields: FormField[] = [
{ label: "Name", name: "name", type: "text", placeholder: "Enter your name" },
{ label: "Gender", name: "gender", type: "select", placeholder: "", options: ["Male", "Female", "Other"] },
];
//Định nghĩa hàm xử lý khi form được submit
const onSubmit: SubmitHandler<SimpleFormData> = (data: SimpleFormData) => {
console.log("Form submitted:", data);
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="container mt-5">
<h2 className="mb-4">Simple Dynamic Form</h2>
{/* Các field sẽ được render tại đây */}
<button type="submit" className="btn btn-primary">
Submit
</button>
</form>
);
onSubmit: SubmitHandler<SimpleFormData> = (data: SimpleFormData) => { ... }: hàm sẽ được gọi khi user nhấn nút submit và form đã pass tất cả các validation rules. Data sẽ là một JavaScript object có cấu trúc giống với SimpleFormData, chứa tất cả các giá trị từ form.
Render Các Trường Form bằng map()
Đây là bước quan trọng nhất, chúng ta chuyển đổi array fields thành các phần tử dynamic UI. Chúng ta sẽ sử dụng phương thức map() của JavaScript.<form onSubmit={handleSubmit(onSubmit)} className="container mt-5">
<h2 className="mb-4">Simple Dynamic Form</h2>
{fields.map((field) => (
<div className="mb-3" key={field.name}>
<label className="form-label">{field.label}</label>
{field.type === "select" ? (
<select
{...register(field.name as keyof SimpleFormData, { required: `${field.label} is required` })}
className="form-select"
>
<option value="">-- Select --</option>
{field.options?.map((opt) => (
<option key={opt} value={opt}>
{opt}
</option>
))}
</select>
) : (
<input
type="text"
placeholder={field.placeholder}
{...register(field.name as keyof SimpleFormData, { required: `${field.label} is required` })}
className="form-control"
/>
)}
{errors[field.name as keyof SimpleFormData] && (
<div className="text-danger mt-1">{errors[field.name as keyof SimpleFormData]?.message}</div>
)}
</div>
))}
<button type="submit" className="btn btn-primary">
Submit
</button>
</form>
Giải thích:
fields.map((field) => (...))
Đây là phương thức JavaScript để lặp qua từng đối tượng field trong mảng fields.
Với mỗi field, nó sẽ trả về một khối JSX đại diện cho một trường nhập liệu hoàn chỉnh.
key={field.name}: Khi bạn render một danh sách các phần tử trong React, mỗi phần tử được tạo ra bởi map phải có một thuộc tính key duy nhất
{...register(field.name as keyof SimpleFormData, { required: \${field.label} required` })}`: đăng ký input/select với react-hook-form.
field.name as keyof SimpleFormData: Đảm bảo rằng field name bạn đang đăng ký khớp với các thuộc tính trong SimpleFormData.
{ required: \${field.label} required` }: Đăng ký required rule. Bạn có thể thêm nhiều validation rules khác ở đây (ví dụ: minLength, maxLength, pattern).
Dưới đây là code toàn bộ của Dynamic Form
import { useForm, type SubmitHandler } from 'react-hook-form';
import type { FormField } from '../../types/formFields';
import type { SimpleFormData } from './SimpleFormData';
const DynamicForm = () => {
const { register, handleSubmit, formState: { errors } } = useForm<SimpleFormData>();
const fields: FormField[] = [
{ label: "Name", name: "name", type: "text", placeholder: "Enter your name" },
{ label: "Gender", name: "gender", type: "select", placeholder: "", options: ["Male", "Female", "Other"] },
];
const onSubmit: SubmitHandler<SimpleFormData> = (data: SimpleFormData) => {
console.log("Form submitted:", data);
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="container mt-5">
<h2 className="mb-4">Simple Dynamic Form</h2>
{fields.map((field) => (
<div className="mb-3" key={field.name}>
<label className="form-label">{field.label}</label>
{field.type === "select" ? (
<select
{...register(field.name as keyof SimpleFormData, { required: `${field.label} is required` })}
className="form-select"
>
<option value="">-- Select --</option>
{field.options?.map((opt) => (
<option key={opt} value={opt}>
{opt}
</option>
))}
</select>
) : (
<input
type="text"
placeholder={field.placeholder}
{...register(field.name as keyof SimpleFormData, { required: `${field.label} is required` })}
className="form-control"
/>
)}
{errors[field.name as keyof SimpleFormData] && (
<div className="text-danger mt-1">{errors[field.name as keyof SimpleFormData]?.message}</div>
)}
</div>
))}
<button type="submit" className="btn btn-primary">
Submit
</button>
</form>
);
}
export default DynamicForm;
Nhận xét
Đăng nhận xét