dimah-formv0.2.0

Forms

Author questionnaires with defineForm, field rules, showWhen, and $Infer.

Forms are TypeScript (defineForm) or rows in your database (saveForm). Code-authored forms feed $Infer. Database forms do not.


Authoring Forms in TypeScript

We recommend defining each questionnaire in a dedicated file under lib/forms/:

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

export const onboardingForm = defineForm({
  title: "Employee Onboarding",
  slug: "onboarding",
  description: "Complete your profile and equipment preferences.",
  status: "active",
  fields: [
    {
      id: "fullName",
      type: "text",
      label: "Full Name",
      required: true,
      minLength: 2,
      maxLength: 100,
    },
    {
      id: "department",
      type: "select",
      label: "Department",
      required: true,
      options: [
        { value: "engineering", label: "Engineering" },
        { value: "design", label: "Product Design" },
        { value: "marketing", label: "Marketing" },
      ],
    },
    {
      id: "githubHandle",
      type: "text",
      label: "GitHub Username",
      required: true,
      showWhen: {
        field: "department",
        equals: "engineering",
      },
    },
    {
      id: "portfolioUrl",
      type: "text",
      label: "Portfolio Link",
      showWhen: {
        field: "department",
        equals: "design",
      },
    },
    {
      id: "newsletter",
      type: "boolean",
      label: "Subscribe to company updates",
      defaultValue: true,
    },
  ],
});

Export your forms from a central index:

lib/forms/index.ts
import { onboardingForm } from "./onboarding";

export const forms = {
  onboarding: onboardingForm,
};

Pass the forms dictionary into dimahForm():

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

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

export type Form = typeof form;

Form Properties

PropertyTypeDefaultDescription
titlestringHuman-readable title of the questionnaire.
slugstringCatalog keyUnique URL-friendly slug. getForm and startResponse accept id or slug.
descriptionstringundefinedOptional subtitle or instructions for respondents.
status"active" | "draft" | "archived""active"Only "active" forms can start new response sessions.
fieldsFormField[][]Array of field definitions.
metaRecord<string, unknown>{}Custom JSON dictionary for UI layout hints, icons, or category tags.

Form Status Lifecycle

  • active: Form is published and accepts new responses via startResponse.
  • draft: Form is still being designed. Starting a response returns 409 FORM_INACTIVE.
  • archived: Form is deprecated. Existing submitted responses remain readable, but new responses are blocked.

Field Anatomy

Each item in fields requires an id, a type, and an optional label:

{
  id: "workEmail",
  type: "email",
  label: "Work Email Address",
  required: true,
  defaultValue: "user@example.com",
  meta: {
    placeholder: "you@company.com",
    tooltip: "We will send your onboarding packet here.",
  },
}

Common Field Attributes

  • id: Unique string identifier within the form. Serves as the key in the answers dictionary.
  • type: Built-in type ("text", "number", "boolean", "select", "multiSelect", "email", "date") or a custom type registered via defineFieldType.
  • label: Display label for UI components (retrieved via fieldLabel(field)).
  • required: If true, the field must be non-empty when submitting the form (if visible).
  • defaultValue: Seeded into the answers map upon calling startResponse.
  • showWhen: Conditional visibility rules.
  • meta: Arbitrary metadata for UI customization (e.g., placeholder, step number, layout width).

Conditional Visibility (showWhen)

Fields can dynamically show or hide based on the current answers. When a field is hidden, it is excluded from visibleFields and its answer is stripped before final submission.

1. Simple Conditions

// Check exact value match
showWhen: { field: "role", equals: "manager" }

// Check inequality
showWhen: { field: "plan", notEquals: "free" }

// Check if a multiSelect array contains a specific option
showWhen: { field: "techStack", includes: "typescript" }

2. Compound Conditions (all / any)

// AND condition (all must match)
showWhen: {
  all: [
    { field: "experience", equals: "senior" },
    { field: "location", equals: "remote" },
  ],
}

// OR condition (at least one must match)
showWhen: {
  any: [
    { field: "department", equals: "engineering" },
    { field: "department", equals: "product" },
  ],
}

Code-Authored vs Database Forms

FeatureCode-Authored Forms (forms: {})Dynamic Database Forms (saveForm())
DeclarationTypeScript files via defineFormAdmin UI / CMS via saveForm API endpoint
Type InferenceFull compile-time $Infer typingRecord<string, unknown>
Version ControlTracked in Git alongside application codeStored in the questionnaire database table
Overwrite ProtectionProtected from accidental API deletion / updatesCan be modified or deleted via API routes

End-to-End Type Inference ($Infer)

When you export export type Form = typeof form, dimah-form automatically derives the exact shape of your answers:

import type { Form } from "@/lib/form";

type OnboardingAnswers = Form["$Infer"]["answers"]["onboarding"];

// TypeScript automatically infers:
// type OnboardingAnswers = {
//   fullName: string;
//   department: "engineering" | "design" | "marketing";
//   githubHandle?: string;
//   portfolioUrl?: string;
//   newsletter?: boolean;
// }

Use this type in your React client:

const q = useFormResponse<Form["$Infer"]["answers"]["onboarding"]>({
  snapshot: formSnapshot,
});

Next Steps

On this page