Builder Concepts

Form Field Reactivity

Three-layer reactive system for form fields: declarative actions, sandboxed expressions, and computed fields.

Form Field Reactivity

Tagixo forms support three levels of reactivity — from declarative rules you configure in the builder drawer, to sandboxed expression evaluation, to PHP-registered callbacks for server-side lookups. Levels 1 and 2 are pure builder features requiring no additional code in your Laravel app. Level 3 requires an admin panel SDK; see the Filament or Primix SDK documentation sections if you need it.

Quick reference

Capability Level 1 — Declarative Level 2 — Expression Level 3 — Registry
Show / hide a field Yes Yes Yes
Require / unrequire Yes Yes Yes
Enable / disable Yes Yes Yes
Set another field's value No Yes (set()) Yes
Computed math No Yes Yes
External data / DB lookup No No Yes
Zero PHP required Yes Yes No
Where configured Drawer → Reactivity Drawer → Reactivity → Expression SDK ReactivityRegistry

Where reactivity is configured

Every form field exposes a Reactivity tab in the builder drawer. Inside it you will find three JSON hook keys:

Key Fires when… Typical use
on_change The field value changes (debounced 300 ms for text inputs) Recalculate totals, reveal dependent fields
on_blur Focus leaves the field Format a display value, validate a pattern
on_load The form first renders, and after each page navigation in multi-step forms Pre-fill from URL parameters, set computed defaults

Select, checkbox, radio, and date fields fire on_change immediately on commit. Text and textarea inputs are debounced 300 ms to avoid reacting on every keystroke.

Each hook accepts an array of action objects evaluated in order. If two actions in the same hook write to the same target field, the last one wins.


Level 1 — Declarative actions

Declarative actions require no code. You paste JSON into the hook field in the drawer and the form runtime handles the rest. The minimum shape of an action object is { "type": "<action>" }. Conditional behaviour is added via a when clause.

Action types

type Effect Required extra keys
set_value Sets this field's value value
set_value_from Copies another field's current value into this field source_field
clear_value Empties this field
show_field Makes a target field visible target_field
hide_field Hides a target field (value is preserved) target_field
require_field Marks a target field required target_field
unrequire_field Removes the required constraint from a target field target_field
enable_field Enables a disabled target field target_field
disable_field Disables (greys out) a target field target_field

when clause

A when clause makes an action conditional. The clause shape is:

{ "field": "<key>", "operator": "<op>", "value": "<v>" }

An action without a when clause fires unconditionally every time its parent hook runs.

Operator Meaning
equals Strict equality
not_equals Strict inequality
is_empty Value is empty string, null, or []
is_not_empty Opposite of is_empty
contains String or array contains the given value
starts_with String starts with the given value
ends_with String ends with the given value
> Numeric greater-than
< Numeric less-than

Common patterns

Show a field only when another field has a specific value

Paste this into the on_change hook of the customer_type field. Set company_name to hidden by default in its own drawer settings so it starts invisible.

[
  {
    "type": "show_field",
    "target_field": "company_name",
    "when": { "field": "customer_type", "operator": "equals", "value": "business" }
  },
  {
    "type": "hide_field",
    "target_field": "company_name",
    "when": { "field": "customer_type", "operator": "not_equals", "value": "business" }
  }
]

Conditionally require a field

Two actions in the same hook handle both branches explicitly. Omitting the unrequire_field branch means the field stays required once triggered, even if the user changes their selection back.

[
  {
    "type": "require_field",
    "target_field": "vat_number",
    "when": { "field": "customer_type", "operator": "equals", "value": "business" }
  },
  {
    "type": "unrequire_field",
    "target_field": "vat_number",
    "when": { "field": "customer_type", "operator": "not_equals", "value": "business" }
  }
]

Clear a dependent field when a parent field changes

When a country dropdown changes, the previously selected city is no longer valid. Clearing it immediately prevents stale data from reaching your controller.

{
  "type": "clear_value",
  "target_field": "city",
  "when": { "field": "country", "operator": "is_not_empty" }
}

Copy a value from another field

{
  "type": "set_value_from",
  "target_field": "billing_address",
  "source_field": "shipping_address",
  "when": { "field": "same_address", "operator": "equals", "value": "1" }
}

Level 2 — Sandboxed expressions

When declarative actions are not expressive enough — computed totals, formatted strings derived from multiple fields, or conditional logic with arithmetic — you can write a sandboxed expression. Expressions are evaluated by a server-side Symfony ExpressionLanguage sandbox delivered to the client as a pre-compiled evaluation tree. They are not eval() and do not execute arbitrary JavaScript.

Security model

The sandbox has no access to window, document, fetch, XMLHttpRequest, timers, cookies, localStorage, or session storage. It cannot import modules or call any function outside the built-in list below. The only permitted side-effect is set(), and it is scoped to fields within the same form.

Infinite loop guard. When field A's expression calls set("B", …), field B's own on_change hook fires once the current expression completes. If B's hook also writes back to A you will create an infinite loop. Wrap the write in an if() equality guard: if(get("B") != computed_value, set("B", computed_value), null).

Built-in functions

Function Signature Description
get(key) get("price") Returns the current value of another field
set(key, value) set("total", 99) Sets another field's value as a side-effect; returns null
number_format(n, decimals) number_format(1234.5, 2) Returns "1,234.50"
concat(...parts) concat("Hello ", get("name")) String concatenation
format_currency(amount, symbol) format_currency(9.9, "€") Returns "€9.90"
if(cond, then, else) if(get("qty") > 0, "ok", "") Ternary
round(n, decimals) round(1.234, 2) Returns 1.23
floor(n) floor(1.9) Returns 1
ceil(n) ceil(1.1) Returns 2
len(value) len(get("notes")) String or array length
upper(str) upper(get("code")) Uppercase
lower(str) lower(get("email")) Lowercase
trim(str) trim(get("name")) Strip leading/trailing whitespace

Expression examples

Compute a subtotal from quantity and unit price

Place this expression in the on_change hook of both the quantity and unit_price fields. The result key holds the expression; the runtime evaluates it and writes the return value into the field that owns the hook.

{
  "type": "expression",
  "result": "number_format(get(\"quantity\") * get(\"unit_price\"), 2)",
  "target_field": "subtotal"
}

Build a formatted display name from first and last name

{
  "type": "expression",
  "result": "concat(trim(get(\"first_name\")), \" \", trim(get(\"last_name\")))",
  "target_field": "display_name"
}

Apply a discount tier based on quantity

{
  "type": "expression",
  "result": "if(get(\"quantity\") >= 100, format_currency(get(\"unit_price\") * 0.85, \"€\"), if(get(\"quantity\") >= 50, format_currency(get(\"unit_price\") * 0.90, \"€\"), format_currency(get(\"unit_price\"), \"€\")))",
  "target_field": "effective_price"
}

Show a character counter as a side-effect and keep the field visible

An expression can combine a return value with set() calls. The return value goes into the field that owns the hook; set() calls update other fields.

{
  "type": "expression",
  "result": "set(\"chars_remaining\", 500 - len(get(\"message\")))"
}

Mixing declarative actions and expressions in one hook

A hook array can contain any combination of declarative action objects and expression objects. They run in order.

[
  {
    "type": "expression",
    "result": "set(\"total\", round(get(\"qty\") * get(\"price\"), 2))"
  },
  {
    "type": "show_field",
    "target_field": "discount_note",
    "when": { "field": "qty", "operator": ">", "value": "49" }
  }
]

Level 3 — PHP-registered callbacks

Level 3 allows expressions to call named PHP functions you register on the server — useful for database lookups, external API calls, or any logic that cannot run in the browser sandbox. Registering these callbacks requires an admin panel SDK (Filament or Primix). See the Filament or Primix SDK documentation sections for the registration API and lifecycle details.


See also