Rules Engine
The rules engine evaluates declarative IRule[] arrays attached to each field in IFormConfig.fields. When a field value changes, the engine re-evaluates all transitively affected fields and updates the runtime form state.
Rule Structure
interface IRule {
/** Condition to evaluate against current form values */
when: ICondition;
/** Effects to apply when the condition is true */
then: IFieldEffect;
/** Effects to apply when the condition is false (optional) */
else?: IFieldEffect;
/** Higher number wins when multiple rules set the same property */
priority?: number;
/** Optional identifier for tracing */
id?: string;
}Supported Effects (IFieldEffect)
| Property | Type | Description |
|---|---|---|
required | boolean | Override required state |
hidden | boolean | Override hidden state |
readOnly | boolean | Override read-only state |
label | string | Override field label |
type | string | Swap component type |
options | IOption[] | Replace dropdown options |
validate | IValidationRule[] | Replace validation rules |
computedValue | string | Override computed value expression |
fieldOrder | string[] | Override field display order |
setValue | unknown | Directly set the field's value (see below) |
fields | Record<string, IFieldEffect> | Apply effects to OTHER fields |
setValue Effect
setValue lets a rule directly set a field's value when its condition is met. This is useful for auto-populating fields based on selections elsewhere in the form.
Key behaviors:
setValueonly applies from thethenbranch -- theelsebranch cannot set a value- Multiple rules with
setValueon the same field use priority-based conflict resolution (highest priority wins) - The effect is stored as
pendingSetValueonIRuntimeFieldStateand applied by the form component viareact-hook-form'ssetValue-- this prevents rule re-evaluation in the same pass (no infinite loops)
Example
{
version: 2,
fields: {
country: {
type: "Dropdown",
label: "Country",
options: [
{ value: "US", label: "United States" },
{ value: "CA", label: "Canada" },
],
},
dialCode: {
type: "Textbox",
label: "Dial Code",
rules: [
{
when: { field: "country", operator: "equals", value: "US" },
then: { setValue: "+1", readOnly: true },
},
{
when: { field: "country", operator: "equals", value: "CA" },
then: { setValue: "+1", readOnly: true },
},
],
},
},
}When the user selects "United States", the dialCode field is automatically set to "+1" and made read-only.
Clearing a field
Use setValue: null to clear a field's value when a condition is met:
rules: [
{
when: { field: "mode", operator: "equals", value: "reset" },
then: { setValue: null },
},
]Cross-field setValue
setValue can also be applied to other fields via the fields cross-effect property:
rules: [
{
when: { field: "billingSameAsShipping", operator: "equals", value: true },
then: {
fields: {
billingCity: { setValue: "$values.shippingCity" },
billingZip: { setValue: "$values.shippingZip" },
},
},
},
]Note:
setValueaccepts a literal value. For dynamic computed values based on other fields, usecomputedValuewith an expression string instead.
Priority-based Conflict Resolution
When multiple rules fire simultaneously and set the same property, the rule with the highest priority number wins (first-write-wins after sorting by priority descending). Rules without a priority default to 0.
rules: [
{ when: { field: "plan", operator: "equals", value: "free" }, then: { hidden: true }, priority: 1 },
{ when: { field: "admin", operator: "equals", value: true }, then: { hidden: false }, priority: 10 },
]
// If both conditions are true, hidden: false wins (priority 10 > 1)Cross-field Effects
A rule on one field can affect multiple other fields using the fields map:
{
type: "Toggle",
label: "Advanced Mode",
rules: [
{
when: { field: "advancedMode", operator: "equals", value: true },
then: {
fields: {
debugPanel: { hidden: false },
logLevel: { required: true },
apiKey: { readOnly: false },
},
},
},
],
}Lifecycle
- Init:
evaluateAllRules(fields, values)-> builds dependency graph + evaluates all rules ->IRuntimeFormState - Validate:
validateDependencyGraph()checks for circular/self-dependencies via Kahn's algorithm - Trigger: Field value change ->
processFieldChange() - Evaluate:
evaluateAffectedFields(changedField, fields, values, currentState)-- re-evaluates only transitively affected fields - Resolve: Priority-based conflict resolution (higher priority rule wins)
- Apply: Dispatch to reducer -> React re-render -> fields read updated
IRuntimeFieldState
