@karnak19/next-form

0.0.2 • Public • Published

@karnak19/next-form

A lightweight, type-safe library for building full-stack forms in Next.js with automatic field generation, Zod validation, and seamless server action integration.

Features

  • 🎯 Type-safe - Full TypeScript support with Zod schema inference
  • 🚀 Automatic field generation - Forms generate themselves based on your schema
  • 🔧 Flexible mapping - Map Zod types to your custom field components
  • 🌐 Server actions - Built-in integration with Next.js server actions
  • 🎨 Custom fields - Support for unique field types and custom components
  • 📦 Minimal setup - Get started with just a few lines of code
  • 🔍 Built on react-hook-form - Leverages the power of react-hook-form under the hood

Installation

npm install @karnak19/next-form zod react-hook-form @hookform/resolvers
# or
yarn add @karnak19/next-form zod react-hook-form @hookform/resolvers
# or
pnpm add @karnak19/next-form zod react-hook-form @hookform/resolvers

Quick Start

1. Create Your Field Components

// components/TextField.tsx
import { useField } from "next-form/client";

export default function TextField() {
  const { field, label, error } = useField<string>();

  return (
    <div>
      <label>{label}</label>
      <input type="text" {...field} />
      {error && <p style={{ color: "red" }}>{error}</p>}
    </div>
  );
}
// components/CheckboxField.tsx
import { useField } from "next-form/client";

export function CheckboxField() {
  const { field, label, error } = useField<boolean>();

  return (
    <div>
      <label>
        <input type="checkbox" {...field} />
        {label}
      </label>
      {error && <p style={{ color: "red" }}>{error}</p>}
    </div>
  );
}

2. Create Schema-to-Component Mapping

// lib/form-mapping.ts
import { z } from "zod";
import { createForm } from "next-form/client";
import TextField from "../components/TextField";
import { CheckboxField } from "../components/CheckboxField";

const mapping = [
  [z.string(), TextField],
  [z.boolean(), CheckboxField],
] as const;

export const Form = createForm({ mapping });

3. Define Your Schema

// lib/schema.ts
import { z } from "zod";

export const userSchema = z.object({
  name: z.string().min(2, "Name must be at least 2 characters"),
  email: z.string().email("Invalid email address"),
  isActive: z.boolean(),
});

export type UserFormData = z.infer<typeof userSchema>;

4. Create a Server Action

// actions/user-action.ts
"use server";

import { createAction } from "next-form/server";
import { userSchema } from "../lib/schema";

export const createUserAction = createAction()
  .input(userSchema)
  .handler(async ({ input }) => {
    // Input is automatically validated and type-safe
    console.log("Creating user:", input);

    // Your database logic here
    // const user = await db.user.create(input);

    return { success: true, user: input };
  });

5. Use the Form

// app/user-form/page.tsx
import { Form } from "../../lib/form-mapping";
import { createUserAction } from "../../actions/user-action";
import { userSchema } from "../../lib/schema";

export default function UserFormPage() {
  return (
    <Form
      schema={userSchema}
      action={createUserAction}
      initialValues={{
        name: "",
        email: "",
        isActive: false,
      }}
      renderSubmit={({ isSubmitting, submit }) => (
        <button type="button" onClick={submit} disabled={isSubmitting}>
          {isSubmitting ? "Creating..." : "Create User"}
        </button>
      )}
    />
  );
}

Advanced Usage

Custom Field Types with Unique Schemas

Create specialized field types that are distinct from their underlying Zod type:

// lib/unique-fields.ts
import { z } from "zod";
import { createUniqueFieldSchema } from "next-form/client";

// Create a select field that's different from a regular string
export const zSelect = createUniqueFieldSchema(z.string(), "select");
export const zTextarea = createUniqueFieldSchema(z.string(), "textarea");
// components/SelectField.tsx
import { useField } from "next-form/client";

export function SelectField() {
  const { field, label, error } = useField<string>();

  return (
    <div>
      <label>{label}</label>
      <select {...field}>
        <option value="">Choose an option</option>
        <option value="option1">Option 1</option>
        <option value="option2">Option 2</option>
      </select>
      {error && <p style={{ color: "red" }}>{error}</p>}
    </div>
  );
}
// Update your mapping
import { zSelect } from "../lib/unique-fields";
import { SelectField } from "../components/SelectField";

const mapping = [
  [z.string(), TextField],
  [z.boolean(), CheckboxField],
  [zSelect, SelectField], // Unique field mapping
] as const;

Server Actions with Middleware (Procedures)

Create reusable middleware for authentication, logging, etc:

// lib/procedures.ts
"use server";

import { createProcedure } from "next-form/server";

// Authentication middleware
const authProcedure = createProcedure(() => {
  // Your auth logic here
  return {
    user: {
      id: "user-123",
      name: "John Doe",
    },
  };
});

// Chain middleware
const adminProcedure = authProcedure.pipe((ctx) => {
  // Check if user is admin
  return {
    ...ctx,
    isAdmin: true,
  };
});

// Create actions with context
export const createAdminAction = adminProcedure
  .createAction()
  .input(someSchema)
  .handler(async ({ input, ctx }) => {
    // ctx contains user and isAdmin
    console.log(`User ${ctx.user.name} is creating:`, input);
    return { success: true };
  });

Custom Field with Props

Field components can accept additional props:

// components/SelectField.tsx
interface SelectFieldProps {
  options?: { value: string; label: string }[];
  placeholder?: string;
}

export function SelectField({ options = [], placeholder }: SelectFieldProps) {
  const { field, label, error } = useField<string>();

  return (
    <div>
      <label>{label}</label>
      <select {...field}>
        {placeholder && <option value="">{placeholder}</option>}
        {options.map((option) => (
          <option key={option.value} value={option.value}>
            {option.label}
          </option>
        ))}
      </select>
      {error && <p style={{ color: "red" }}>{error}</p>}
    </div>
  );
}

Handling Form Events

export default function UserFormPage() {
  const handleSuccess = (data: any) => {
    console.log("Form submitted successfully:", data);
    // Redirect, show toast, etc.
  };

  const handleError = (error: Error) => {
    console.error("Form submission failed:", error);
    // Show error message
  };

  return (
    <Form
      schema={userSchema}
      action={createUserAction}
      initialValues={{ name: "", email: "", isActive: false }}
      onSuccess={handleSuccess}
      onError={handleError}
      renderSubmit={({ isSubmitting, submit }) => (
        <button type="button" onClick={submit} disabled={isSubmitting}>
          {isSubmitting ? "Submitting..." : "Submit"}
        </button>
      )}
    />
  );
}

API Reference

Client-side (next-form/client)

createForm({ mapping })

Creates a Form component with automatic field generation based on schema-to-component mapping.

Parameters:

  • mapping: Array of tuples mapping Zod schemas to React components

Returns: Form component

useField<T>(name?)

Hook for connecting field components to form state.

Parameters:

  • name: Field name (optional if used within FieldProvider context)

Returns:

{
  field: {
    name: string;
    value: T;
    onChange: (value: T) => void;
    onBlur: () => void;
    ref: RefCallback;
  };
  label: string; // Auto-generated from field name
  error?: string;
}

createUniqueFieldSchema<T>(schema, id)

Creates a unique field schema for specialized field types.

Parameters:

  • schema: Base Zod schema
  • id: Unique identifier string

Returns: Unique field schema

Server-side (next-form/server)

createAction()

Creates a basic action builder for form submission.

Returns: Action builder with input() and handler() methods

createProcedure(middleware)

Creates a procedure with middleware that can be chained.

Parameters:

  • middleware: Function that processes context

Returns: Chainable procedure builder

Form Props

interface FormProps<TFieldValues, TOutput> {
  schema: z.ZodTypeAny;
  action: ActionDefinition<TFieldValues, TOutput>;
  initialValues?: Partial<TFieldValues>;
  onSuccess?: (data: TOutput) => void;
  onError?: (error: Error) => void;
  renderSubmit: (props: SubmitRenderProps) => ReactElement;
}

Examples

Check out the /src/examples directory for complete working examples including:

  • Basic text and checkbox fields
  • Custom select fields with unique schemas
  • Server actions with and without middleware
  • Form integration examples

Requirements

  • Next.js 15+
  • React 19+
  • TypeScript
  • Zod 3.25+
  • react-hook-form 7+

License

MIT

Readme

Keywords

none

Package Sidebar

Install

npm i @karnak19/next-form

Weekly Downloads

4

Version

0.0.2

License

none

Unpacked Size

29.8 kB

Total Files

19

Last publish

Collaborators

  • bazeso