Authoring Forms
Build Form Definitions with type-aware field, layout, and rule helpers.
The durable Schemaform document is still a plain Form Definition:
{
formatVersion: 1,
schema: {},
uiSchema: {},
rules: []
}defineForm is a developer-facing authoring layer on top of that document. It lets application code describe fields, pages, layout, and rules with a smaller API, then returns the same persisted envelope that the renderer, builder, parser, migration code, and rule engine already understand.
Use the helpers when you are writing forms in TypeScript. Use the raw FormDefinition shape when you are importing JSON, migrating stored definitions, or building lower-level tooling that already edits JSON Schema and UI Schema directly.
Define Fields
Fields are the source of type inference. Once a key exists in fields, helper callbacks can suggest and check that key in pages, layout, and rules.
import { defineForm, field } from "@mackehansson/schemaform";
const contactForm = defineForm({
fields: {
name: field.text({ title: "Name", required: true, minLength: 2 }),
email: field.email({
title: "Email",
required: true,
meta: { iconText: "Please provide an email" },
}),
message: field.textarea({ title: "Message" }),
},
pages: ({ page }) => [page("Contact", ["name", "email", "message"])],
});If you type page("Contact", ["..."]) in an editor, the array items are checked against the field keys. A typo like "emial" is a type error instead of a broken form discovered later.
Add Renderer Metadata
Use meta for application-owned presentation data that Schemaform should carry but not interpret:
const profileForm = defineForm({
fields: {
email: field.email({
title: "Email",
required: true,
meta: {
iconText: "Please provide an email",
icon: "mail",
},
}),
},
});The generated Control keeps that metadata, and the resolved control exposes it to custom renderers and widgets:
function EmailWidget({ control }: { control: ResolvedControl }) {
return <p>{control.meta?.iconText}</p>;
}The top-level field options stay strict so TypeScript can still catch typos like requried. Custom renderer data belongs under meta.
For stronger app-level typing, augment SchemaformMeta:
import "@mackehansson/schemaform";
declare module "@mackehansson/schemaform" {
interface SchemaformMeta {
iconText?: string;
icon?: "mail" | "user" | "building";
}
}Compose Layout
The pages callback receives helpers for common UI Schema nodes. You can pass field keys directly, or group them into layout nodes.
const applicationForm = defineForm({
fields: {
firstName: field.text({ title: "First name", required: true }),
lastName: field.text({ title: "Last name", required: true }),
email: field.email({ title: "Email", required: true }),
companyName: field.text({ title: "Company" }),
members: field.array({
title: "Team members",
itemFields: {
name: field.text({ title: "Name", required: true }),
role: field.text({ title: "Role" }),
},
}),
},
pages: ({ page, group, columns, repeater }) => [
page("Application", [
group("Applicant", [columns(["firstName", "lastName"]), "email"]),
group("Company", ["companyName"], {
meta: { icon: "building" },
}),
repeater("members", ["name", "role"]),
]),
],
});The output is normal UI Schema. A custom renderer does not need to know that helpers created it.
Add Rules
Rules are still interpreted by the Schemaform rule engine at runtime. The helper API only makes authoring them less string-heavy.
const signupForm = defineForm({
fields: {
plan: field.select({
title: "Plan",
options: ["Free", "Enterprise"],
}),
companyName: field.text({ title: "Company name" }),
},
pages: ({ page, group }) => [
page("Signup", [
"plan",
group("Enterprise details", ["companyName"], {
id: "enterprise-details",
}),
]),
],
rules: ({ rule, target }) => [
rule.when("plan").equals("Enterprise").show("companyName"),
rule.when("plan").equals("Enterprise").require("companyName"),
rule
.when("plan")
.notEquals("Enterprise")
.hide(target("enterprise-details")),
],
});Use a field key when the rule targets a control. Use target("layout-id") when the rule targets a page, group, columns node, or another layout id.
Build Custom Builders
The helpers are useful in product code, but they also clarify how to build your own builder UI:
- Your builder can store and edit the persisted
FormDefinition. - Your builder can use helpers in command handlers, templates, test fixtures, and starter definitions.
- Your visual rule editor can still use the lower-level rule condition helpers when users need nested groups, custom operators, or JSONLogic import/export.
For example, a template picker in a builder can create a definition with defineForm, while the drag-and-drop canvas later updates the same definition with the pure builder mutations from schemaform/core.
Drop Down A Level
The helper layer is intentionally optional. Reach for the lower-level APIs when you need exact control:
import {
addField,
addPage,
checkRenderability,
migrate,
parseDefinition,
resolveForm,
} from "@mackehansson/schemaform/core";Those APIs operate on the same document that defineForm returns, so you can mix both styles without converting between models.