OWolf

BlogToolsProjectsAboutContact
© 2025 owolf.com
HomeAboutNotesContactPrivacy

2025-06-01 Web Development

Building a Dynamic Form System with Configuration-Driven Design in Next.js

By O. Wolfson

Modern web applications often require a flexible system to manage forms—capable of adapting to changing data structures and supporting reusability across different entity types.

Source code

Demo

✨ Why Config-Driven Forms?

By defining form structure in configuration files, we decouple form rendering logic from form content, enabling:

  • Easy addition of new forms without changing the core rendering code.
  • Separation of concerns (structure vs logic).
  • Custom component support per field (e.g. special editors, selectors).
  • Reusability across domains like inventory, transactions, users, etc.

🧪 How It Works

A typical setup includes:

1. FieldConfig Types

Each field in the form is described using a common config schema like:

ts
type FieldType =
  | "text"
  | "textarea"
  | "checkbox"
  | "select"
  | "date"
  | "custom"
  | "json-editor";

type FieldConfig = {
  name: string;
  label: string;
  type: FieldType;
  placeholder?: string;
  Component?: (props: { formValues: any }) => JSX.Element; // for custom rendering
};

2. Entity Config Files

Each formable entity defines a config:

ts
export const entityConfig = {
  schema: z.object({
    title: z.string().min(1),
    notes: z.string().optional(),
  }),
  fields: [
    { name: "title", label: "Title", type: "text" },
    { name: "notes", label: "Notes", type: "textarea" },
  ],
};

3. DynamicForm Component

The core renderer reads the config and uses react-hook-form to manage state and validation.


🧪 Tutorial: Create a Dynamic Form System in Next.js (App Router)

We'll build a minimal form builder from scratch.

✅ Step 1: Project Setup

bash
npx create-next-app@latest dynamic-forms --app --typescript
cd dynamic-forms
npm install zod react-hook-form @hookform/resolvers

✅ Step 2: Define Field Types

Create src/form/types.ts

ts
export type FieldType = "text" | "textarea" | "json-editor";

export type FieldConfig = {
  name: string;
  label: string;
  type: FieldType;
};

export type EntityConfig = {
  schema: any;
  fields: FieldConfig[];
};

✅ Step 3: Dynamic Form Renderer

Create src/components/DynamicForm.tsx

tsx
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import type { EntityConfig } from "../form/types";

export default function DynamicForm({
  config,
  onSubmit,
}: {
  config: EntityConfig;
  onSubmit: (values: any) => void;
}) {
  const form = useForm({
    resolver: zodResolver(config.schema),
  });

  return (
    <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
      {config.fields.map((f) => (
        <div key={f.name}>
          <label>{f.label}</label>
          {f.type === "text" && <input {...form.register(f.name)} />}
          {f.type === "textarea" && <textarea {...form.register(f.name)} />}
        </div>
      ))}
      <button type="submit">Submit</button>
    </form>
  );
}

✅ Step 4: Create Sample Entities

ts
// src/entities/note.ts
import { z } from "zod";

export const noteConfig = {
  schema: z.object({
    title: z.string(),
    content: z.string().optional(),
  }),
  fields: [
    { name: "title", label: "Title", type: "text" },
    { name: "content", label: "Content", type: "textarea" },
  ],
};
ts
// src/entities/task.ts
import { z } from "zod";

export const taskConfig = {
  schema: z.object({
    task: z.string(),
    detail: z.string().optional(),
  }),
  fields: [
    { name: "task", label: "Task", type: "text" },
    { name: "detail", label: "Detail", type: "textarea" },
  ],
};

✅ Step 5: Use in a Route

tsx
// app/page.tsx
import DynamicForm from "@/components/DynamicForm";
import { noteConfig } from "@/entities/note";

export default function Page() {
  return (
    <main className="p-6">
      <h1>Note Form</h1>
      <DynamicForm config={noteConfig} onSubmit={console.log} />
    </main>
  );
}

✅ Benefits

  • Clean separation between structure and render logic.
  • Easily extendable.
  • Powerful for admin panels, schema-driven UIs, CMS tools.