Tagixo Docs

Developer Documentation for Laravel, SDK integrations, and extensibility

Extending Tagixo

Custom Form Modules

Build form fields and wrappers with FormModule/FormModuleWrapper and map validation cleanly.

Custom Form Modules

For form context, use dedicated abstractions:

  • FormModule for field modules
  • FormModuleWrapper for container/wrapper modules

These abstractions are form-specific and should not be documented as generic page modules.

Scaffold with generators

Two artisan commands scaffold the starting skeleton:

php artisan make:tagixo-form-field VatField --field-type=text --icon=heroicon-o-identification
php artisan make:tagixo-form-wrapper WizardStep --field-type=wrapper

Default destinations (configurable via config('tagixo.scaffolding')):

  • Fields → app/tagixo/FormFields/VatField.php namespace App\Tagixo\FormFields
  • Wrappers → app/tagixo/FormWrappers/WizardStep.php namespace App\Tagixo\FormWrappers

Per-command overrides (--path= + --namespace=) and package-mode (--package=path/to/plugin) work identically to make:tagixo-module. After scaffolding, the command prints the registration snippet for config/tagixo.php modules array (or the plugin's service provider in package mode).

Form modules and wrappers are full participants in the unified component registry — they are NOT a separate registry from page modules.

Minimum contract for field modules

Implement or provide identity values for:

  • label
  • icon
  • field type
  • type ID
  • define()

Example field module

use Ccast\Tagixo\Core\ModuleDefinition;
use Ccast\Tagixo\Core\Props\TextProp;
use Ccast\Tagixo\FormBuilder\FormModule;

class VatFieldModule extends FormModule
{
    public static function getLabel(): string { return __('VAT'); }
    public static function getIcon(): string { return 'heroicon-o-identification'; }
    public static function getFieldType(): string { return 'text'; }
    public static function getTypeId(): string { return 'vat-field'; }

    public static function define(): ModuleDefinition
    {
        return ModuleDefinition::make()
            ->tab('content', __('Content'), [
                TextProp::make('name')->default('vat'),
                TextProp::make('label')->default('VAT Number'),
                static::columnSpanProp(),
            ])
            ->tab('validation', __('Validation'), [
                ...static::commonValidationProps(),
            ])
            ->contexts(['form'])
            ->meta(['isFormField' => true]);
    }
}

Important notes:

  • contexts(['form']) is mandatory
  • validation fields belong in a dedicated validation tab
  • runtime schema is generated from this definition

What toSchema() is for

Form modules are not only preview elements. They also have to map to runtime form schema.

For field modules, the output usually includes:

  • type
  • name
  • label
  • placeholder/helper/default values when present
  • validation data when present
  • optional layout metadata

That is why form modules need stricter discipline than visual-only page modules.

Wrapper modules

Use wrappers for grouped/structured layouts in form context: grid, fieldset/section, wizard, wizard step, tabs, tab. Built-in wrappers ship under Ccast\Tagixo\FormBuilder\Modules\* and you extend FormModuleWrapper for your own.

A wrapper's contract differs from a field in three important ways:

  • getCategory(): string returns 'wrapper' (default in the base class).
  • isWrapper(): bool returns true.
  • allowsChildren(): bool returns true — children are stored under props.children[] in the canonical structure.

You typically also override:

  • getAllowedChildren(): array — return an allow-list of child type IDs if the wrapper restricts what can go inside. Return [] to allow any. Built-in wrappers leave this open; specialty wrappers (e.g. "Pricing matrix row") may lock it down.
  • toSchema(array $props): array — wrappers must recurse into their children, calling ComponentRegistry::get($child['type'])::toSchema($child['props']) for each, and return their own metadata plus a children array. The base class provides a compileChildren() helper that does this recursion correctly.
class WizardStep extends FormModuleWrapper
{
    public static function getLabel(): string { return __('Wizard Step'); }
    public static function getIcon(): string { return 'heroicon-o-list-bullet'; }
    public static function getFieldType(): string { return 'wizard-step'; }
    public static function getTypeId(): string { return 'wizard-step'; }

    public static function getAllowedChildren(): array
    {
        // Only allow fields and the grid wrapper inside a wizard step
        return ['text', 'textarea', 'select', 'checkbox', 'radio', 'grid'];
    }

    public static function define(): ModuleDefinition
    {
        return ModuleDefinition::make()
            ->tab('content', __('Content'), [
                TextProp::make('label')->setLabel(__('Step Label')),
                TextProp::make('description')->setLabel(__('Step Description')),
            ])
            ->contexts(['form'])
            ->canvas('cards');
    }
}

Validation pipeline

Form submissions hit POST /tagixo/forms/{formKey}/submit (handled by FormSubmissionController). The controller does NOT hardcode validation rules — it asks each field class for them, in this order:

  1. Look up the field's PHP class via ComponentRegistry::get($field['type'])
  2. If the class declares extractValidationRules(array $props): array, call it
  3. Otherwise fall back to FormModule::extractValidationRules($props) which reads the validation tab keys (required, min, max, pattern, ...) and emits Laravel rule arrays

This means the pipeline is SDK-aware automatically — if a downstream SDK (or your own application) registers a custom field class with its own validation logic, the controller picks it up without configuration.

class VatField extends FormModule
{
    // ... getLabel, getIcon, getFieldType, getTypeId, define() ...

    public static function extractValidationRules(array $props): array
    {
        $rules = parent::extractValidationRules($props);  // standard rules first
        $rules[] = new \App\Rules\ItalianVat();           // add domain rule
        return $rules;
    }
}

The validation tab on the drawer can also use static::commonValidationProps() to get the shared Required / Min / Max / Pattern toggles — wrap them in array_merge() with your field-specific extras.

Reactivity

Every form field that extends FormModule automatically gets a Reactivity tab in the drawer. Editors can wire one field to update another using a three-layer model: declarative actions, sandboxed expressions, and a plugin registry for custom helpers.

You do NOT need to do anything in the module class to enable reactivity — the base class define() chain handles it. If your custom field exposes a special signal that callers may want to react to (e.g. a "Calculate" button whose click should populate a sibling field), document the keys consumers should target in target_field on the Reactivity tab.

See the Form Field Reactivity page for the full action shape, transforms, expression sandbox, and the TagixoReactivity facade.

Registration

Register through config('tagixo.modules') (FQCN array) or runtime Tagixo::registerModule(\App\Tagixo\FormFields\YourField::class). Wrappers go through the same registration — they live in the same registry as fields.

If you scaffolded with make:tagixo-form-field or make:tagixo-form-wrapper, the command prints the registration snippet for you.

Runtime schema mapping

  • Ensure toSchema() output is coherent for runtime forms.
  • Keep validation extraction aligned with your validation tab config.
  • Keep wrapper children ordering stable.

Testing checklist

  • visible only in form context
  • content + validation tabs present
  • generated runtime schema is valid
  • preview rendering works
  • saved JSON remains stable after reload
  • validation rules are not silently dropped
  • wrapper children survive nesting and export correctly