Tagixo Docs

Developer Documentation for Laravel, SDK integrations, and extensibility

Builder Concepts

Form Field Reactivity

Three-layer reactive system for form fields: declarative actions, sandboxed expressions, and an extensible plugin registry.

Form Field Reactivity

Form fields in Tagixo support reactive behavior — fields that update each other in response to user input. The system has three layers, each more powerful than the previous, and you pick the lowest layer that does the job. Higher layers exist for what the lower ones cannot express.

The three layers

  1. Declarative actions — set / clear / copy values with conditions and transforms. No PHP. Covers about 80% of CRUD reactivity.
  2. Sandboxed expressions — Symfony ExpressionLanguage with a whitelisted built-in library. Computed values, conditionals, string transforms.
  3. Plugin registry — register additional functions into the sandbox from your application code. Extensibility hook for domain-specific helpers.

Where it lives in the drawer

Every form field that extends FormModule shows a Reactivity tab in the drawer alongside Content / Validation / Design. The tab writes to:

props.reactivity = {
    live: bool,
    live_mode: 'instant' | 'on_blur' | 'debounced',
    debounce: int,                   // ms
    on_state_updated: [Action, ...],
    on_state_hydrated: [Action, ...],
    before_dehydrated: [Action, ...],
}

At Filament render time, that JSON is rehydrated into afterStateUpdated, afterStateHydrated, and beforeStateDehydrated closures attached to the live field.

Level 1 — Declarative actions

Each action is a flat associative array:

[
    'action_type'        => 'set_value' | 'clear_field' | 'copy_state' | 'set_expression',
    'target_field'       => string,          // field name to write to
    'static_value'       => ?string,         // for set_value
    'expression'         => ?string,         // for set_expression
    'transform'          => 'none' | 'upper' | 'lower' | 'slug' | 'trim' | 'int' | 'float' | 'bool',
    'condition_field'    => ?string,         // blank = always run
    'condition_operator' => '==' | '!=' | 'empty' | 'not_empty',
    'condition_value'    => ?string,
]

Action types

Type What it does
set_value Writes transform(static_value) to target_field
clear_field Writes null to target_field
copy_state Writes transform($currentFieldState) to target_field
set_expression Writes transform(eval(expression)) to target_field

Transforms

none, upper, lower, slug, trim, int, float, bool. All applied to the value produced by the action before it hits the target field.

Conditions

Every action can be gated by one comparison against another field. Operators: ==, !=, empty, not_empty. Leave condition_field blank to always run.

Equality comparisons are intentionally loose (boolean-aware, numeric-aware, otherwise string-cast) because the drawer collects everything as strings. This matches user intuition ("true" == true is true).

Level 2 — Sandboxed expressions

For values that need real computation, set action_type = 'set_expression' and write a Symfony ExpressionLanguage expression in the expression field.

Available variables

Name Meaning
state Current value of the field that triggered the hook
_get Filament Get helper (used internally by get(path))

Built-in functions

Function Purpose
get(path) Read another field's value
slug(v), upper(v), lower(v), title(v), trim(v) String transforms
length(v) mb_strlen
contains(h, n), starts_with(h, n), ends_with(h, n) String predicates
replace(v, search, replace) String replace
coalesce(...) First non-null argument
if(cond, a, b) Conditional
int(v), float(v), bool(v) Type casts
min(...), max(...), round(v, p) Arithmetic

Adding to this list requires a code change — that's the whole point of the sandbox. For runtime-registered helpers, use Level 3.

Example expressions

# Auto-fill display name from first + last
get('first_name') ~ ' ' ~ get('last_name')

# Generate username slug from email local-part
slug(replace(get('email'), '@', '-'))

# Conditional default
coalesce(get('display_name'), get('email'), 'anonymous')

# Length-gated transform
length(state) > 5 ? upper(state) : state

Level 3 — Plugin registry

When the built-ins don't cover your domain (e.g. VAT validation, in-house currency formatters, tenant lookup), register custom functions through the facade:

use Ccast\TagixoFilament\Facades\TagixoReactivity;

// Single function
TagixoReactivity::register('vat_format', fn (string $vat) => strtoupper(trim($vat)));

// Bulk
TagixoReactivity::registerMany([
    'currency' => fn (float $amount) => number_format($amount, 2, ',', '.'),
    'fiscal_year' => fn (string $date) => (int) substr($date, 0, 4),
]);

Provider classes

For larger function sets or per-tenant function bundles, implement ReactivityFunctionProvider:

use Ccast\TagixoFilament\FormBuilder\Reactivity\Contracts\ReactivityFunctionProvider;
use Symfony\Component\ExpressionLanguage\ExpressionFunction;

class AcmeReactivityProvider implements ReactivityFunctionProvider
{
    public function getFunctions(): array
    {
        return [
            ExpressionFunction::fromPhp('strtoupper', 'shout'),
            new ExpressionFunction(
                'tenant_id',
                fn () => 'null',
                fn () => app('current.tenant')->id,
            ),
        ];
    }
}

TagixoReactivity::addProvider(new AcmeReactivityProvider());

Providers can be deferred — registered to evaluate on each request rather than at boot — which is how you ship per-tenant function sets without leaking one tenant's helpers into another.

Security model

What the sandbox prevents:

  • Method calls on objects
  • The new operator
  • Access to PHP built-ins that aren't on the whitelist
  • eval, exec, filesystem, environment variables

What the sandbox does NOT protect against:

  • Functions YOU register. Anything you push through TagixoReactivity::register() runs as regular PHP with full privileges. You own the contract — don't accept closures as arguments, don't mutate global state, don't make blocking I/O calls.
  • Runaway computations. The sandbox is Turing-incomplete (no loops, no recursion) but very deep expressions still cost CPU. Keep expressions short.

Putting it together

A common pattern: a "Slug" field that auto-fills from a "Title" field when empty:

  1. On the Title field's Reactivity tab, add an action:
    • action_type = set_expression
    • expression = slug(state)
    • target_field = slug
    • condition_field = slug, condition_operator = empty
  2. Save the form. The Title field will populate slug on every keystroke, but only when slug is currently empty — once the user types into the slug field manually, the auto-fill stops.

That single declarative chain replaces 15 lines of imperative closure code.