Form Builder

Form Context

Use the form builder context in Filament, exclude irrelevant PropTypes, and enable app-target forms.

Form Context

The Tagixo builder supports a form context that switches the entire builder shell into form-authoring mode. This document covers every aspect of using the form context inside a Filament (or Primix) admin panel.


1. Setting getContext() to 'form'

Override getContext() on your builder page class to activate form mode:

protected function getContext(): string
{
    return 'form';
}

This single change affects every dimension of the builder UI. The table below shows what differs between the two contexts:

Dimension page context form context
Module palette All registered modules Form-specific buckets only
Canvas drop target Full layout canvas Form canvas (no layout nesting)
Design drawer panels All panels Subset (see §4)
Preview mode Full page render Form embed or app-target render
Toolbar actions Page-level save / publish Form schema save
Right-click context menu Section / Row / Column Field / Group / Fieldset
Keyboard shortcuts Standard Standard (same)
Slug / SEO tab Visible Hidden
Global variables tab Visible Hidden

2. Form Module Palette

The palette is split into two buckets.

Universal modules

These always render via the shared form-embed-node partial and work in every environment (plain HTML, Livewire, Inertia, etc.).

Module field_type Notes
Text input text Single-line <input type="text">
Textarea textarea Multi-line <textarea>
Select select <select> with options
Checkbox checkbox Single boolean checkbox
Radio group radio <input type="radio"> group
Date date <input type="date">
File upload file <input type="file">
Submit button submit <button type="submit">
Grid grid Layout column wrapper
Fieldset fieldset Semantic <fieldset> + <legend>

App-target modules

These require a registered AppFormPreviewer to render in the canvas. Without one, they degrade gracefully to a placeholder in universal preview.

Module field_type Notes
Tabs tabs Filament Tabs component
Wizard wizard Filament Wizard component
Group group Filament Group component

3. App-target modules and Tagixo::enableAppForms()

When the Tagixo plugin boots, it calls Tagixo::enableAppForms() automatically as part of plugin registration. This call:

  • registers the AppFormPreviewer contract binding
  • enables the Tabs, Wizard, and Group modules in the palette
  • wires up the /tagixo/form-preview/{key} internal route used by the canvas iframe

If you are building a form builder for a non-Filament frontend (e.g. a plain Blade form, an API-consumed schema, or a headless SPA), you can disable app-target modules:

// In your AppServiceProvider or plugin registration
Tagixo::disableAppForms();

This removes the Tabs / Wizard / Group palette entries and skips the previewer route registration. Universal modules still work normally.


4. excludedCanvasPropTypes()

Override this method to suppress design panels that are irrelevant inside a form canvas. The method returns an array of PropType class names to hide from the drawer.

protected function excludedCanvasPropTypes(): array
{
    return [
        \Ccast\Tagixo\PropTypes\AnimationPropType::class,
        \Ccast\Tagixo\PropTypes\BoxShadowPropType::class,
        \Ccast\Tagixo\PropTypes\FilterPropType::class,
        \Ccast\Tagixo\PropTypes\TransformPropType::class,
    ];
}

Recommended exclusions for form context:

PropType Reason to exclude
AnimationPropType Scroll/entrance animations are meaningless on form fields
BoxShadowPropType Box shadow on inputs conflicts with browser focus rings
FilterPropType CSS filters (blur, grayscale) degrade form usability
TransformPropType CSS transforms break stacking context for dropdowns / date pickers

You may keep SpacingPropType, BorderPropType, TypographyPropType, and BackgroundPropType — they all apply meaningfully to form elements.


5. Two Preview Modes

Universal HTML preview

Always available, no extra configuration required. The canvas renders every field using the form-embed-node partial, which emits plain semantic HTML. App-target modules (Tabs, Wizard, Group) degrade to a labelled placeholder block showing the module name and field count.

Use this mode when:

  • you are building a form that will be rendered server-side as HTML
  • you have not registered a Filament app previewer
  • you want a fast, dependency-free preview

App-target preview

Requires a registered previewer (see §6). When active, the canvas iframe loads your previewer route, which renders the form schema as real Filament components inside a Livewire page. The preview updates on every save.

Use this mode when:

  • the form will be embedded into a Filament panel as a live component
  • you need accurate rendering of Tabs / Wizard / Group layout
  • your QA process involves stakeholders who need to interact with the real form

6. Registering the Filament App Form Previewer

Route

Add a named route inside your admin panel route group (or wherever your panel routes live):

Route::get('/tagixo/form-preview/{key}', [FormPreviewController::class, 'show'])
    ->name('tagixo.form-preview')
    ->middleware(['web', 'auth']);

FormPreviewController

<?php

namespace App\Http\Controllers\Admin;

use Ccast\Tagixo\Models\FormSchema;
use Illuminate\Http\Request;

class FormPreviewController extends Controller
{
    public function show(Request $request, string $key)
    {
        abort_unless($request->user()?->can('viewAny', FormSchema::class), 403);

        $schema = FormSchema::where('key', $key)->firstOrFail();

        return view('admin.form-preview', ['schema' => $schema]);
    }
}

Security note: always gate on a real authorization check. Never render a schema by key without verifying the authenticated user has read access to form schemas. The key parameter comes from the builder iframe URL and must be treated as untrusted input.

Blade view (resources/views/admin/form-preview.blade.php)

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    @livewireStyles
    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="bg-white dark:bg-gray-950">
    <livewire:form-preview-wrapper :schema="$schema" />
    @livewireScripts
</body>
</html>

FormPreviewWrapper Livewire component

<?php

namespace App\Livewire;

use Ccast\Tagixo\Models\FormSchema;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Forms\Form;
use Livewire\Component;

class FormPreviewWrapper extends Component implements HasForms
{
    use InteractsWithForms;

    public FormSchema $schema;

    public ?array $data = [];

    public function mount(FormSchema $schema): void
    {
        $this->schema = $schema;
        $this->form->fill();
    }

    public function form(Form $form): Form
    {
        return $form
            ->schema($this->schema->toFilamentSchema())
            ->statePath('data');
    }

    public function render()
    {
        return view('livewire.form-preview-wrapper');
    }
}
{{-- resources/views/livewire/form-preview-wrapper.blade.php --}}
<div class="p-6">
    {{ $this->form }}
</div>

Auth callout: the FormPreviewWrapper component itself does not re-check authorization — it trusts the controller gate. Never register this component on a public Livewire route.


7. Complete Example

A production-ready EditFormSchemaBuilder page with all methods implemented.

Builder page class

<?php

namespace App\Primix\Resources\FormSchemas\Pages;

use Ccast\TagixoPrimix\Resources\FormSchemas\Pages\EditFormSchemaBuilder as BaseEditFormSchemaBuilder;
use Ccast\Tagixo\PropTypes\AnimationPropType;
use Ccast\Tagixo\PropTypes\BoxShadowPropType;
use Ccast\Tagixo\PropTypes\FilterPropType;
use Ccast\Tagixo\PropTypes\TransformPropType;

class EditFormSchemaBuilder extends BaseEditFormSchemaBuilder
{
    protected function getContext(): string
    {
        return 'form';
    }

    protected function excludedCanvasPropTypes(): array
    {
        return [
            AnimationPropType::class,
            BoxShadowPropType::class,
            FilterPropType::class,
            TransformPropType::class,
        ];
    }

    protected function getPreviewUrl(): ?string
    {
        return route('tagixo.form-preview', ['key' => $this->record->key]);
    }
}

Resource page registration

Register your override in the FormSchemaResource (or wherever the base resource registers its pages):

public static function getPages(): array
{
    return [
        'index'   => Pages\ListFormSchemas::route('/'),
        'create'  => Pages\CreateFormSchema::route('/create'),
        'edit'    => Pages\EditFormSchema::route('/{record}/edit'),
        'builder' => EditFormSchemaBuilder::route('/{record}/builder'),
    ];
}

Table action linking to the builder

Add this action to your FormSchemaResource table:

use Filament\Tables\Actions\Action;

Action::make('open_builder')
    ->label('Open Builder')
    ->icon('heroicon-o-pencil-square')
    ->url(fn (FormSchema $record): string =>
        static::getUrl('builder', ['record' => $record])
    ),