This document provides a comprehensive guide to implementing a dynamic, nested profile builder form. The form includes functionality for adding, removing, and reordering fields dynamically. It is built using React Hook Form, Zod for validation, and React Beautiful DnD for drag-and-drop reordering. Styling ensures responsiveness and a smooth user experience.
To build the profile builder, reusable form components such as Select
, Input
, Textarea
, and DatePicker
were created. These components utilize the Controller
component from React Hook Form to integrate seamlessly with the form state.
const FormInput = ({ name, control, ...props }: FormInputProps) => {
return (
<Controller
name={name}
control={control}
render={({
field: { onChange, onBlur, value, ref },
fieldState: { error, invalid },
}) => (
<div>
<Input
className="lg:w-[240px]"
onChange={onChange}
placeholder="Type something..."
errorMessage={error?.message}
{...props}
/>
<div className="text-red-600 text-sm mt-2 ml-2">{error?.message}</div>
</div>
)}
/>
);
};
export default FormInput;
Dynamic fields are managed using useFieldArray
from React Hook Form. This allows seamless addition and removal of fields while maintaining form state.
The main profile form uses a useFieldArray
to manage categories dynamically.
const { control } = useFormContext();
const { fields, append, remove } = useFieldArray({
name: 'categories',
control,
});
<Button onClick={() => append({ title: '', fields: [] })}>Add Category</Button>
<ul>
{fields.map((field, index) => (
<li key={field.id} className="mb-4">
<Input name={`categories.${index}.title`} control={control} placeholder="Category Title" />
<Button onClick={() => remove(index)}>Remove</Button>
</li>
))}
</ul>
Inside each category, additional fields are managed using another useFieldArray
. A component, CreateCategoryField
, handles these subfields dynamically.
const CreateNewCategoryField = ({ categoryIndex, control }) => {
const { append, remove, fields, move } = useFieldArray({
name: `category.${categoryIndex}.fields`,
control,
shouldUnregister: true,
});
return (
<ul>
{fields.map((field, index) => (
<li key={field.id} className="flex gap-2 mb-2">
<Input
name={`categories.${categoryIndex}.fields.${index}.name`}
control={control}
placeholder="Field Name"
/>
<Button onClick={() => remove(index)}>Remove</Button>
</li>
))}
<Button onClick={() => append({ name: '' })}>Add Field</Button>
</ul>
);
};
Form validation is powered by Zod and integrated with React Hook Form using zodResolver
. Conditional validation is implemented using a z.discriminatedUnion
to handle optional and required fields dynamically.
import { z } from 'zod';
const fieldSchema = z.discriminatedUnion('isRequired', [
z.object({
isRequired: z.literal(false),
name: z.string().optional(),
}),
z.object({
isRequired: z.literal(true),
name: z.string().nonempty('This field is required'),
}),
]);
const categorySchema = z.object({
title: z.string().nonempty('Category title is required'),
fields: z.array(fieldSchema),
});
const profileSchema = z.object({
categories: z.array(categorySchema),
});
To enable reordering, React Beautiful DnD is combined with the move
method from useFieldArray
.
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
const handleDragDrop = ({ source, destination }) => {
if (destination) {
move(source.index, destination.index);
}
};
<DragDropContext onDragEnd={handleDragDrop}>
<Droppable droppableId="categories" direction="vertical">
{(provided) => (
<ul ref={provided.innerRef} {...provided.droppableProps}>
{fields.map((field, index) => (
<Draggable key={field.id} draggableId={field.id} index={index}>
{(provided) => (
<li
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className="flex flex-col border p-2 mb-4"
>
<Input
name={`categories.${index}.title`}
control={control}
placeholder="Category Title"
/>
<CreateCategoryField categoryIndex={index} control={control} />
</li>
)}
</Draggable>
))}
{provided.placeholder}
</ul>
)}
</Droppable>
</DragDropContext>;
These images demonstrate how users can interact with the form, dynamically add/remove fields, reorder items, and submit the form. The form is optimized for both desktop and mobile views.
This is a template for creating applications using Next.js 14 (app directory) and HeroUI (v2).
To create a new project based on this template using create-next-app
, run the following command:
npx create-next-app -e https://github.com/heroui-inc/next-app-template
You can use one of them npm
, yarn
, pnpm
, bun
, Example using npm
:
npm install
npm run dev
If you are using pnpm
, you need to add the following code to your .npmrc
file:
public-hoist-pattern[]=*@heroui/*
After modifying the .npmrc
file, you need to run pnpm install
again to ensure that the dependencies are installed correctly.
Licensed under the MIT license.