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.