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-templateYou can use one of them npm, yarn, pnpm, bun, Example using npm:
npm installnpm run devIf 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.

