Custom Form Modules
For form context, use dedicated abstractions:
FormModulefor field modulesFormModuleWrapperfor 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.phpnamespaceApp\Tagixo\FormFields - Wrappers →
app/tagixo/FormWrappers/WizardStep.phpnamespaceApp\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:
typenamelabel- 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(): stringreturns'wrapper'(default in the base class).isWrapper(): boolreturnstrue.allowsChildren(): boolreturnstrue— children are stored underprops.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, callingComponentRegistry::get($child['type'])::toSchema($child['props'])for each, and return their own metadata plus achildrenarray. The base class provides acompileChildren()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:
- Look up the field's PHP class via
ComponentRegistry::get($field['type']) - If the class declares
extractValidationRules(array $props): array, call it - Otherwise fall back to
FormModule::extractValidationRules($props)which reads thevalidationtab 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
formcontext - 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