dimah-formv0.2.0

Custom Fields

Add a server validator with defineFieldType. The UI widget stays in your app.

Custom field types are server validators, not React components. You define how the answer is checked and typed. You render whatever control you want.


1. Defining a Custom Field Type

Use defineFieldType() from @dimah-form/core to declare a new field validator:

lib/field-types/rating.ts
import { defineFieldType } from "@dimah-form/core";
import * as z from "zod";

export const ratingFieldType = defineFieldType({
  type: "rating",

  // 1. Zod schema for validating the field's configuration in defineForm()
  fieldSchema: z.looseObject({
    type: z.literal("rating"),
    min: z.number().int().min(1).default(1),
    max: z.number().int().max(10).default(5),
  }),

  // 2. Server validator for respondent answer values
  validate: (value, field) => {
    if (typeof value !== "number" || !Number.isInteger(value)) {
      return {
        message: "Rating must be an integer",
        code: "EXPECTED_INTEGER",
      };
    }

    const min = field.min ?? 1;
    const max = field.max ?? 5;

    if (value < min) {
      return {
        message: `Rating must be at least ${min}`,
        code: "TOO_SMALL",
        params: { min },
      };
    }

    if (value > max) {
      return {
        message: `Rating cannot exceed ${max}`,
        code: "TOO_LARGE",
        params: { max },
      };
    }

    return undefined; // Valid!
  },

  // 3. Helper to determine if the field is empty for `required` checks
  isEmpty: (value) => value == null,

  // 4. Type token for TypeScript compile-time answer inference
  $Infer: 0 as number,
});

Export your custom field types from a central module:

lib/field-types/index.ts
import { ratingFieldType } from "./rating";

export const customFieldTypes = [ratingFieldType] as const;

2. Registering on Server and Client

Register your custom field types in both dimahForm() and createFormClient():

Server Registration

lib/form.ts
import { dimahForm, memoryAdapter } from "@dimah-form/server";
import { customFieldTypes } from "@/lib/field-types";
import { forms } from "@/lib/forms";

export const form = dimahForm({
  database: memoryAdapter(),
  fieldTypes: customFieldTypes,
  forms,
});

export type Form = typeof form;

Client Registration

lib/form-client.ts
"use client";

import { createFormClient } from "@dimah-form/react";
import { customFieldTypes } from "@/lib/field-types";
import type { Form } from "@/lib/form";

export const formClient = createFormClient<Form>({
  basePath: "/api/form",
  fieldTypes: customFieldTypes,
});

export const { useFormResponse } = formClient;

3. Using Custom Fields in Forms

Define questionnaires with your new field type:

lib/forms/review.ts
import { defineForm } from "@dimah-form/server";

export const reviewForm = defineForm({
  title: "Product Review",
  slug: "review",
  fields: [
    {
      id: "score",
      type: "rating",
      label: "Star Rating (1-5)",
      required: true,
      min: 1,
      max: 5,
    },
    {
      id: "comments",
      type: "text",
      label: "Review Comments",
    },
  ],
});

TypeScript automatically infers Form["$Infer"]["answers"]["review"]["score"] as number.


4. Rendering Custom Widgets in React

Add a branch to your FormFieldControl dispatcher to render an interactive star rating control:

components/form-field-control.tsx
import { type FormFieldBinding } from "@dimah-form/react";

export function FormFieldControl(binding: FormFieldBinding) {
  const { field, value, error, disabled, required, onChange } = binding;
  if (!field) return null;

  // Custom Rating Widget
  if (field.type === "rating") {
    const currentRating = typeof value === "number" ? value : 0;
    const max = typeof field.max === "number" ? field.max : 5;

    return (
      <div className="space-y-1.5">
        <label className="block text-sm font-semibold">
          {field.label}
          {required && <span className="ml-1 text-red-500">*</span>}
        </label>
        <div className="flex gap-1">
          {Array.from({ length: max }, (_, i) => i + 1).map((star) => (
            <button
              key={star}
              type="button"
              disabled={disabled}
              onClick={() => onChange(star)}
              className={`text-2xl transition-colors ${
                star <= currentRating ? "text-yellow-400" : "text-gray-300"
              }`}
            >

            </button>
          ))}
        </div>
        {error && <p className="text-xs text-red-500">{error}</p>}
      </div>
    );
  }

  // Other field types...
  return null;
}

Next Steps

On this page