Skip to content

Guides

Dynamic rules

Most real forms are conditional: a field appears only for a certain country, a section unlocks once a box is ticked, a total recomputes as quantities change. In FormKrafter that behavior lives in the spec as rules, so it survives being stored, migrated and re-validated on the server.

A rule has a trigger and a list of effects. When the trigger evaluates to strictly true, its effects apply:

type Rule = {
name: string
type: 'jsonLogic' | 'javaScript'
logic?: RulesLogic // when type is 'jsonLogic'
code?: string // when type is 'javaScript'
effects?: Effect[]
}

Each effect targets one property of the brick:

Target Type Effect
hidden boolean removes the field from view and from validation
disabled boolean renders it read-only
required boolean makes it mandatory conditionally
value computed overwrites the field’s value

Serializable, inspectable, safe to store and diff:

Hide unless the country is FR or DE
{
"name": "eu-only",
"type": "jsonLogic",
"logic": { "!": { "in": [{ "var": "country" }, ["FR", "DE"]] } },
"effects": [
{ "property": { "target": "hidden", "type": "boolean" }, "boolean": true }
]
}

When the logic gets awkward to express declaratively:

return dataMap.quantity > 10 ? 'GOLD' : 'STANDARD'

dataMap is the whole form’s data, keyed by brick key. The builder’s rule editor autocompletes dataMap. with your form’s actual field keys.

JavaScript rules are never passed to eval. They are parsed with Acorn and walked by an AST interpreter:

  • CSP-safe — no unsafe-eval directive needed anywhere in your app.
  • Isolated — only dataMap (and value inside custom validators), plus whitelisted builtins: Math, JSON, String, Number, Date, Array, Object, Boolean. No fetch, no globalThis, no constructor or __proto__ escape.
  • Expressive enough — expressions, ternaries, const/let, if/return, template literals, optional chaining, arrow functions (.filter(x => …)).
  • Bounded — unsupported syntax is rejected upfront and runaway code is cut by an execution budget.

You can run the same sandbox yourself:

import { runSandboxed } from '@streamline-pulse/formkrafter-core'
runSandboxed('return items.filter((i) => i > 2)', { items: [1, 2, 3] })
// → [3]

A brick hidden by a rule — or nested inside a hidden parent — is skipped by validation, in the browser and in validateFormData on your server. This is what stops a conditionally hidden required field from blocking a submission forever.

A project by Streamline Pulse