Skip to main content

Rules & Behavior

Rules express conditional behavior in the schema. They run synchronously on every value change, before React renders.

How rules work

Each rule has:

  1. when — a condition expression (evaluated against current field values + context)
  2. one or more effects — what happens when the condition is true

Rules are evaluated by runtime.evaluate(), which runs inside FormRuntimeProvider on every RHF value change.

Writing rules

import { rule, fieldRef, contextRef } from "formwright/schema";

rule.when(expression)

Returns a builder with effect methods. Chain one effect method to complete the rule.

rule.when(fieldRef("accountType").eq("company")).show("companyName")
rule.when(fieldRef("status").eq("locked")).disable("editableField")
rule.when(fieldRef("hasShipping").eq(true)).require("shippingAddress")
rule.when(fieldRef("country").eq("us")).setValue("currency", "USD")
rule.when(fieldRef("promoCode").exists()).clearValue("discount")

fieldRef(field)

References a field value in an expression. Accepts a BuiltField object or a path string.

fieldRef(myField) // BuiltField object
fieldRef("address.city") // dot-notation path string

contextRef(name)

References a value from the context object passed to createFormRuntime.

const runtime = createFormRuntime({
form,
plugins: registerBasicPlugins(),
context: { mode: "view", userRole: "admin" },
});

// In rules:
rule.when(contextRef("mode").eq("view")).disable(nameField)
rule.when(contextRef("userRole").neq("admin")).hide(adminPanel)

Operators

Comparison operators are available on fieldRef() and contextRef() return values.

MethodExpressionExample
.eq(value)EqualsfieldRef(f).eq("active")
.neq(value)Not equalsfieldRef(f).neq("deleted")
.gt(value)Greater thanfieldRef(f).gt(0)
.gte(value)Greater than or equalfieldRef(f).gte(18)
.lt(value)Less thanfieldRef(f).lt(100)
.lte(value)Less than or equalfieldRef(f).lte(999)
.in([...])Value is in listfieldRef(f).in(["a", "b"])
.exists()Non-empty valuefieldRef(f).exists()

Compound conditions

Combine multiple conditions with and, or, and not:

import type { RuleExpression } from "formwright/schema";

// Both conditions must be true:
const condition: RuleExpression = {
and: [
fieldRef(accountType).eq("company"),
fieldRef(employeeCount).gt(50),
],
};

// Either condition must be true:
const condition: RuleExpression = {
or: [
fieldRef(plan).eq("pro"),
fieldRef(plan).eq("enterprise"),
],
};

// Negate a condition:
const condition: RuleExpression = {
not: fieldRef(status).eq("active"),
};

rule.when(condition).show(upgradePrompt)

Effects

Effects are the actions a rule applies when its condition is true.

EffectMethodDescription
show.show(target)Make field visible
hide.hide(target)Hide field
enable.enable(target)Enable a disabled field
disable.disable(target)Disable a field
require.require(target, true)Mark field as required
un-require.require(target, false)Remove required state
setValue.setValue(target, value)Set a field's value
clearValue.clearValue(target)Clear a field's value
disableAll.disableAll()Disable all fields (view mode)

Setting values from rules

// Auto-set currency when country changes:
rule.when(fieldRef("country").eq("us")).setValue("currency", "USD"),
rule.when(fieldRef("country").eq("gb")).setValue("currency", "GBP"),

View mode pattern

Disable all fields when the form is in view mode:

const runtime = createFormRuntime({
form,
plugins: registerBasicPlugins(),
context: { mode: "view" },
});

// In schema:
rules: [
rule.when(contextRef("mode").eq("view")).disableAll(),
]

Multiple rules, same target

Multiple rules can target the same field. The last matching rule wins for each effect type. Design your rules so they don't produce conflicting states (e.g., one rule shows a field while another hides it simultaneously based on the same condition).

Rule evaluation order

Rules are evaluated in the order they appear in the rules array. For the same effect type on the same field, the last matching rule takes precedence.