Skip to main content

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

HelperPurpose
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:

OptionTypeDescription
labelstringDisplay label
descriptionstringShort description below label
helpTextstringExpandable help or tooltip text
placeholderstringInput placeholder text
requiredbooleanInitial required state (can be overridden by rules)
defaultTDefault value
rendererstringOverride the renderer key for this field
componentPropsobjectForwarded 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.