Ý tưởng
Trong thực tế, form không chỉ những thành phần riêng lẻ mà gồm nhiều thành phần, như form KYC gồm có thông tin cá nhân, địa chỉ, thông tin liên hệ, financial status, ...
Lấy ví dụ như thông tin cá nhân, được dùng ở 2 page UserInfo cho officer xem và user xem (hoặc nhiều page khác).
Để tái sử dụng, chúng ta tách form thành các thành phần để có thể tái sử dụng.
Trong quá trình mình làm assignment, mình thấy có 2 cách tách form
- Cách 1: Khai báo props trong từng component con (vd như UserInfo.tsx, AddressInfo.tsx). Trong cách này, mỗi component con tự định nghĩa mảng fields của riêng nó (ví dụ: userInfoFields trong UserInfo.tsx, addressFields trong AddressInfo.tsx).
- Cách 2: Khai báo tất cả props trong một đối tượng duy nhất ở DynamicForm.tsx (Component cha). Trong cách này, DynamicForm.tsx (dựa trên vd part 1) khai báo biến fieldInfos chứa tất cả các định nghĩa trường trong một mảng fields lớn. Sau đó, fieldInfos sẽ filter các field này và truyền các element phù hợp xuống từng component con.
Ở cách 1, có component sẽ tự quản lý các field của mình. Trong khi đó ở cách 2, chỉ khai báo duy nhất ở Dynamic form, dễ dàng quản lý cấu trúc form.
Mình chọn cách 1.
Xây dựng form component
Dựa vào ví dụ ở bài trước, mình sẽ tách UserInfo ra thành component riêng và khai báo trong DynamicForm. Bạn thử làm với AddressInfo nha.src/
├── types/
│ ├── formFields.ts
├── components/
│ ├── DynamicForm.tsx
│ ├── UserInfo.tsx
│ └── AddressInfo.tsx
└── App.tsx
Chúng ta sẽ dùng ComplexFormData thay cho SimpleFormData. ComplexFormData sẽ chứa 2 property UserInfo và AddressInfo đại diện cho data của 2 component
// src/types/formFields.ts
export type InputType = 'text' | 'number' | 'select' | 'tel' | 'email' | 'password' | 'textarea' | 'checkbox' | 'radio' | 'date' | 'time' | 'file';
export interface FormField {
label: string;
name: string;
type: InputType;
placeholder: string;
options?: string[];
}
export interface UserInfoData{
firstName: string;
lastName: string;
email: string;
phone: string;
profileImage: FileList;
}
export interface AddressInfoData {
street: string;
city: string;
zipCode: string;
country: string;
}
export interface ComplexFormData {
userInfo: UserInfoData;
address: AddressInfoData;
}
Khai báo UserInfo
Chúng ta sẽ đi từng bước để hiểu quá trình khai báo và integrate UserInfo vào DynamicForm
Khai báo UserInfo.tsximport React from 'react';
import type { UseFormRegister, FieldErrors, Control } from 'react-hook-form';
import type { ComplexFormData } from '../../types/formFields';
interface UserInfoProps {
register: UseFormRegister<ComplexFormData>;
errors: FieldErrors<ComplexFormData>;
control: Control<ComplexFormData>;
}
const UserInfo : React.FC<UserInfoProps> = ({ register, errors, control }) => {
return (<>
</>);
}
export default UserInfo;
Khai báo UserInfo trong DynamicForm.tsx, pass các props register, errors, control xuống cho component UserInfo
import { useForm, type SubmitHandler } from 'react-hook-form';
import type { ComplexFormData } from '../../types/formFields';
import UserInfo from './UserInfo';
const DynamicForm = () => {
const { register, handleSubmit, control, formState: { errors } } = useForm<ComplexFormData>({
defaultValues: {
userInfo: { firstName: '', lastName: '', email: '', phone: '' },
address: { street: '', city: '', zipCode: '', country: '' },
}
});
const onSubmit: SubmitHandler<ComplexFormData> = (data: ComplexFormData) => {
console.log("Form submitted:", data);
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="container mt-5">
<h2 className="mb-4">Simple Dynamic Form</h2>
<UserInfo register={register} errors={errors} control={control} />
<button type="submit" className="btn btn-primary">
Submit
</button>
</form>
);
}
export default DynamicForm;
Ở file UserInfo, khai báo các field cần được hiển thị
const userInfoFields: FormField[] = [
{ label: 'Họ', name: 'userInfo.firstName', type: 'text', placeholder: 'Last name' },
{ label: 'Tên', name: 'userInfo.lastName', type: 'text', placeholder: 'First Name' },
{ label: 'Email', name: 'userInfo.email', type: 'email', placeholder: 'email@example.com' },
{ label: 'Số điện thoại', name: 'userInfo.phone', type: 'tel', placeholder: '0123-456-789' },
{ label: 'Ảnh đại diện', name: 'userInfo.profileImage', type: 'file', placeholder: '' },
];
Render các field trong userInfoFields
{userInfoFields.map((field) =>
(
<div>
<label htmlFor='{field.name}' className="form-label">{field.label}</label>
<input id={field.name}
type={field.type}
placeholder={field.placeholder}
{...register(field.name as keyof ComplexFormData, { required: `${field.label} is required` })}
className="form-control"
/>
{errors.userInfo?.[field.name.split('.')[1] as keyof ComplexFormData['userInfo']] && (
<div className="text-danger mt-1">{errors.userInfo?.[field.name.split('.')[1] as keyof ComplexFormData['userInfo']]?.message}</div>
)}
</div>
))
}
Dưới đây là toàn bộ source code của DynamicForm và UserInfo
import { useForm, type SubmitHandler } from 'react-hook-form';
Dynamic Form
import type { ComplexFormData } from '../../types/formFields';
import UserInfo from './UserInfo';
const DynamicForm = () => {
const { register, handleSubmit, control, formState: { errors } } = useForm<ComplexFormData>({
defaultValues: {
userInfo: { firstName: '', lastName: '', email: '', phone: '' },
address: { street: '', city: '', zipCode: '', country: '' },
}
});
const onSubmit: SubmitHandler<ComplexFormData> = (data: ComplexFormData) => {
console.log("Form submitted:", data);
const profileImageFile = data.userInfo.profileImage?.[0];
if (profileImageFile) {
console.log("Image name:", profileImageFile.name);
console.log("Size of image:", profileImageFile.size, "bytes");
console.log("File type:", profileImageFile.type);
const formData = new FormData();
formData.append('firstName', data.userInfo.firstName);
formData.append('lastName', data.userInfo.lastName);
formData.append('email', data.userInfo.email);
formData.append('phone', data.userInfo.phone);
formData.append('street', data.address.street);
formData.append('city', data.address.city);
formData.append('zipCode', data.address.zipCode);
formData.append('country', data.address.country);
if (profileImageFile) {
formData.append('profileImage', profileImageFile);
}
try {
alert('Data was sent!');
} catch (error) {
console.error(error);
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="container mt-5">
<h2 className="mb-4">Simple Dynamic Form</h2>
<UserInfo register={register} errors={errors} control={control} />
<button type="submit" className="btn btn-primary">
Submit
</button>
</form>
);
}
export default DynamicForm;
User Info
import React from 'react';
import type { UseFormRegister, FieldErrors, Control } from 'react-hook-form';
import type { ComplexFormData, FormField } from '../../types/formFields';
interface UserInfoProps {
register: UseFormRegister<ComplexFormData>;
errors: FieldErrors<ComplexFormData>;
control: Control<ComplexFormData>;
}
const UserInfo : React.FC<UserInfoProps> = ({ register, errors, control }) => {
const userInfoFields: FormField[] = [
{ label: 'Họ', name: 'userInfo.firstName', type: 'text', placeholder: 'Nguyễn' },
{ label: 'Tên', name: 'userInfo.lastName', type: 'text', placeholder: 'Văn A' },
{ label: 'Email', name: 'userInfo.email', type: 'email', placeholder: 'email@example.com' },
{ label: 'Số điện thoại', name: 'userInfo.phone', type: 'tel', placeholder: '0123-456-789' },
{ label: 'Ảnh đại diện', name: 'userInfo.profileImage', type: 'file', placeholder: '' },
];
return (<>
<h2>User Info</h2>
<div className="card mb-4">
<div className="card-header bg-info text-white">User Information</div>
<div className="card-body">
{userInfoFields.map((field) =>
(
<div>
<label htmlFor='{field.name}' className="form-label">{field.label}</label>
<input id={field.name}
type={field.type}
placeholder={field.placeholder}
{...register(field.name as keyof ComplexFormData, { required: `${field.label} is required` })}
className="form-control"
/>
{errors.userInfo?.[field.name.split('.')[1] as keyof ComplexFormData['userInfo']] && (
<div className="text-danger mt-1">{errors.userInfo?.[field.name.split('.')[1] as keyof ComplexFormData['userInfo']]?.message}</div>
)}
</div>
))
}
</div>
</div>
</>);
}
export default UserInfo;
Nhận xét
Đăng nhận xét