Building a Dynamic Form Generator with Next.js and TailwindCSS
Forms are essential in almost every application, but creating them repeatedly for different use cases can be tedious. In this guide, I’ll show you how to build a dynamic form generator with Next.js and TailwindCSS, enabling developers to easily configure and render forms based on a JSON configuration object.
Why a Form Generator?
A form generator allows developers to:
- Dynamically define fields via configuration.
- Reuse the same component for different forms.
- Handle validation and data management seamlessly.
n this example, we’ll build a FormGenerator
component that:
- Supports multiple field types (
text
,email
,textarea
,select
, andfile
). - Handles form data state automatically.
- Includes default values for fields.
- Uses TailwindCSS for styling.
Project Setup
First, create a new Next.js project
npx create-next-app form-generator
cd form-generator
Building the Form Generator
The FormGenerator
component takes a config
object to define the fields, handles form data state, and renders appropriate inputs based on field types.
FormGenerator Component
Here’s the core of the form generator:
import React, { JSX, useEffect } from 'react';
interface FieldConfig {
type: string;
label: string;
name: string;
placeholder?: string;
required?: boolean;
options?: { value: string; label: string }[];
defaultValue?: string; // Add a defaultValue property
}
interface FormConfig {
fields: FieldConfig[];
}
export const FormGenerator = (
{ config, handleSubmit, children, formData, setFormData }:
{
config: FormConfig,
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void,
children: any,
formData: { [key: string]: any },
setFormData: React.Dispatch<React.SetStateAction<{ [key: string]: any }>>
},
) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData((prev: any) => ({ ...prev, [name]: value }));
};
useEffect(() => {
const initialData: { [key: string]: any } = {};
config.fields.forEach((field) => {
initialData[field.name] = field.defaultValue || '';
});
setFormData(initialData);
}, [config]);
// Map input types to corresponding components
const fieldRenderers: {
[key: string]: (field: FieldConfig) => JSX.Element;
} = {
text: (field) => (
<div key={field.name} className="flex flex-col space-y-2 w-full">
<label htmlFor={field.name} className="font-medium">
{field.label}
</label>
<input
type="text"
name={field.name}
id={field.name}
placeholder={field.placeholder}
required={field.required}
value={formData[field.name] || ''}
onChange={handleChange}
className="p-2 border rounded w-full"
/>
</div>
),
email: (field) => (
<div key={field.name} className="flex flex-col space-y-2 w-full">
<label htmlFor={field.name} className="font-medium">
{field.label}
</label>
<input
type="email"
name={field.name}
id={field.name}
placeholder={field.placeholder}
required={field.required}
value={formData[field.name] || ''}
onChange={handleChange}
className="p-2 border rounded w-full"
/>
</div>
),
file: (field) => (
<div key={field.name} className="flex flex-col space-y-2 w-full">
<label htmlFor={field.name} className="font-medium">
{field.label}
</label>
<input
type="file"
name={field.name}
id={field.name}
required={field.required}
value={formData[field.name] || ''}
onChange={handleChange}
className="p-2 border rounded w-full"
/>
</div>
),
textarea: (field) => (
<div key={field.name} className="flex flex-col space-y-2 w-full">
<label htmlFor={field.name} className="font-medium">
{field.label}
</label>
<textarea
name={field.name}
id={field.name}
placeholder={field.placeholder}
required={field.required}
value={formData[field.name] || ''}
onChange={handleChange}
className="p-2 border rounded w-full resize-none"
></textarea>
</div>
),
select: (field) => (
<div key={field.name} className="flex flex-col space-y-2 w-full">
<label htmlFor={field.name} className="font-medium">
{field.label}
</label>
<select
name={field.name}
id={field.name}
required={field.required}
value={formData[field.name] || field.defaultValue || ''} // Use defaultValue if no value is set
onChange={handleChange}
className="p-2 border rounded w-full bg-white"
>
<option value="">Select an option</option>
{field.options?.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
),
};
return (
<form onSubmit={handleSubmit} className="space-y-4 p-4 border rounded-md">
{config.fields.map((field: any) => ( fieldRenderers[field.type] ? fieldRenderers[field.type](field) : null ))}
{children}
</form>
);
};
Using the Form Generator
import { useState } from "react";
import { FormGenerator } from "./FormGenerator";
const formConfig = {
fields: [
{
type: 'text',
label: 'Name',
name: 'name',
placeholder: 'Enter your name',
required: true,
defaultValue: 'John Doe', // Default value for the text input
},
{
type: 'email',
label: 'Email',
name: 'email',
placeholder: 'Enter your email',
required: true,
defaultValue: 'john.doe@example.com', // Default value for the email input
},
{
type: 'textarea',
label: 'Message',
name: 'message',
placeholder: 'Enter your message',
defaultValue: 'Hello! This is a default message.', // Default value for the textarea
},
{
type: 'select',
label: 'Gender',
name: 'gender',
options: [
{ value: 'male', label: 'Male' },
{ value: 'female', label: 'Female' },
],
required: true,
defaultValue: 'male', // Default selected value for the dropdown
},
{
type: 'file',
label: 'Upload File',
name: 'file',
required: true,
// Default values for file inputs are generally not supported for security reasons
},
],
};
const MainHome = () => {
const [formData, setFormData] = useState<{ [key: string]: any }>({});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log('Form Data:', formData);
};
return (
<div className="min-h-screen flex items-center justify-center">
<FormGenerator
config={formConfig}
handleSubmit={handleSubmit}
formData={formData}
setFormData={setFormData}>
<button type="submit" className="p-2 bg-blue-500 text-white rounded w-full">
Submit
</button>
</FormGenerator>
</div>
);
};
export default MainHome;
Features
- Reusable: Configure forms dynamically with a JSON object.
- Extendable: Add more field types like radio buttons, checkboxes, or date pickers.
- TailwindCSS Styling: Customize the look and feel easily.
Wrapping Up
This form generator demonstrates the power of React and Next.js for building flexible components. With TailwindCSS, styling is a breeze, making it easy to create professional-looking UIs.