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:
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:
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
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
"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:
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:
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;
}