Schema
The schema defines your form: what fields exist, how they're laid out, how they behave. It's plain TypeScript — no React, no side effects.
import { defineForm, field, layout, rule, datasource, buildForm } from "formwright/schema";
Fields
Text fields
field.text("firstName", { label: "First name", required: true })
field.textarea("bio", { label: "Bio", maxLength: 500 })
field.email("contactEmail", { label: "Email" })
field.url("website", { label: "Website" })
field.phone("mobile", { label: "Phone number" })
Numeric fields
field.number("price", { label: "Price", minimum: 0 })
field.integer("quantity", { label: "Quantity", minimum: 1, maximum: 999 })
Boolean
field.checkbox("agreeToTerms", { label: "I agree to the terms" })
Select (single-choice)
// Inline options:
field.select("country", {
label: "Country",
options: [
{ value: "us", label: "United States" },
{ value: "ca", label: "Canada" },
{ value: "gb", label: "United Kingdom" },
],
})
// Options from a datasource (loaded at runtime):
field.select("role", {
label: "Role",
dataSource: "roles", // matches a datasource name in buildForm
})
Date and time
field.date("birthday", { label: "Date of birth" })
field.datetime("scheduledAt", { label: "Scheduled time" })
Array
Object arrays use field.objectArray (shorthand) or field.array with kind: "object":
// Shorthand:
field.objectArray("lineItems", {
label: "Line items",
item: {
name: field.textItem({ label: "Item name" }),
qty: { valueType: "integer", label: "Qty" },
price: { valueType: "number", label: "Price" },
},
minItems: 1,
maxItems: 20,
})
// Primitive array (list of strings):
field.array("tags", {
label: "Tags",
item: field.stringItem(),
})
Array item values use indexed paths: lineItems.0.name, lineItems.1.price.
Array item helpers
| Helper | Purpose |
|---|---|
field.stringItem(defaultValue?) | Primitive string item |
field.numberItem(defaultValue?) | Primitive number item |
field.booleanItem(defaultValue?) | Primitive boolean item |
field.textItem(options?) | Object-array field with valueType: "string" |
Common field options
All field helpers accept:
| Option | Type | Description |
|---|---|---|
label | string | Display label |
description | string | Short description below label |
helpText | string | Expandable help or tooltip text |
placeholder | string | Input placeholder text |
required | boolean | Initial required state (can be overridden by rules) |
default | T | Default value |
renderer | string | Override the renderer key for this field |
componentProps | object | Forwarded to the renderer component |
Layout
Layout nodes build the visual structure. All container nodes accept an optional { title, description, visibleWhen } config.
Stack
Vertical list of nodes.
layout.stack("root", [
layout.field(nameField),
layout.field(emailField),
])
Grid
Multi-column layout. Children can specify a span.
layout.grid("name-grid", { columns: 2 }, [
layout.field(firstNameField, { span: 1 }),
layout.field(lastNameField, { span: 1 }),
layout.field(emailField, { span: 2 }),
])
Section
Named group with a title and border.
layout.section("billing-section", [
layout.field(cardNumberField),
layout.field(expiryField),
], { title: "Billing details" })
Tabs
layout.tabs("form-tabs", [
{
key: "personal",
label: "Personal",
content: layout.stack("personal-stack", [layout.field(nameField)]),
},
{
key: "account",
label: "Account",
content: layout.stack("account-stack", [layout.field(emailField)]),
},
])
Stepper
layout.stepper("checkout", [
{
key: "shipping",
label: "Shipping",
content: layout.stack("shipping-step", [layout.field(addressField)]),
},
{
key: "payment",
label: "Payment",
content: layout.stack("payment-step", [layout.field(cardField)]),
},
])
Divider
layout.divider("section-divider")
Rules
Rules define conditional behavior. See Conditional Fields for full examples.
import { rule, fieldRef, contextRef } from "formwright/schema";
// Show a field when another field equals a value:
rule.when(fieldRef(accountType).eq("company")).show(companyName)
// Require a field conditionally:
rule.when(fieldRef(accountType).eq("company")).require(companyName)
// Use context values (passed to createFormRuntime):
rule.when(contextRef("mode").eq("view")).disable(editableField)
// Compound conditions:
rule.when({ and: [fieldRef(a).eq("x"), fieldRef(b).gt(5)] }).show(targetField)
Data sources
Data sources provide options for field.select fields.
import { datasource } from "formwright/schema";
// Static options:
datasource.static("roles", [
{ value: "admin", label: "Admin" },
{ value: "member", label: "Member" },
])
// Remote API (requires registerAsyncPlugins()):
datasource.remote("countries", {
endpoint: "/api/countries",
method: "GET",
labelKey: "name",
valueKey: "code",
})
See Data Sources for async loading, dependent fields, and custom plugins.
buildForm
Assembles everything into a FormDefinition:
const form = buildForm({
form: defineForm({ id: "checkout" }),
fields: [accountType, companyName, emailField],
layout: layout.stack("root", [
layout.field(accountType),
layout.field(companyName),
layout.field(emailField),
]),
rules: [
rule.when(fieldRef(accountType).eq("company")).show(companyName),
rule.when(fieldRef(accountType).neq("company")).hide(companyName),
],
datasources: [
datasource.static("roles", rolesOptions),
],
});
Pass the result to createFormRuntime.