Form Builder

Form Tables: Columns and Filters

Surface form submissions in Filament tables using FilamentFormColumns, FilamentFormFilters, and FilamentFormStyles.

Form Tables: FilamentFormColumns, FilamentFormFilters, FilamentFormStyles

Overview

When a Tagixo form schema is used as the source for a Filament table (e.g. a submissions resource), three utility classes bridge builder field definitions to Filament table primitives:

Class Responsibility
FilamentFormColumns Maps each builder field type to the appropriate Column class
FilamentFormFilters Maps each builder field type to the appropriate Filter class
FilamentFormStyles Injects per-form CSS into the page in a way that survives Livewire morphing

All three classes live in the Ccast\Tagixo\Filament\Forms namespace and operate on a FormSchema model instance.


The CSS Injection Problem

Livewire's DOM morphing algorithm re-uses existing DOM nodes and discards injected <style> tags that were not present in the initial server render. This means any CSS appended to <head> after page load will be stripped on the next Livewire update cycle.

FilamentFormStyles sidesteps this by wrapping the CSS in a <script> block that re-injects the stylesheet idempotently on every morph:

// The script checks for an existing <style id="..."> before inserting,
// so repeated morphs do not duplicate the tag.
FilamentFormStyles::scriptFrom($formSchema);

Registration

Register the render hook once in a service provider so the script fires at BODY_START before Livewire initialises:

use Filament\Support\Facades\FilamentView;
use Illuminate\Contracts\View\View;

FilamentView::registerRenderHook(
    'panels::body.start',
    fn (): View => view('tagixo::filament.form-styles', [
        'script' => FilamentFormStyles::scriptForForm($formKey),
    ]),
);

Method reference

Method Returns Use when
scriptFrom(FormSchema $form) string (JS snippet) You already have the model
from(FormSchema $form) string (raw CSS) You need the raw CSS for another purpose
forForm(string $formKey) string (raw CSS) You only have the form key
scriptForForm(string $formKey) string (JS snippet) Render hook with form key

FilamentFormColumns

FilamentFormColumns::from(FormSchema $form) returns an array of Filament Column instances, one per visible builder field, in schema order.

Builder-type → Column mapping

Builder field type Filament column class
text TextColumn
textarea TextColumn (truncated)
number TextColumn
email TextColumn
url TextColumn
tel TextColumn
date TextColumn (formatted)
select SelectColumn (readonly)
checkbox IconColumn (boolean)
toggle IconColumn (boolean)
radio TextColumn
file TextColumn (filename only)

Usage

use Ccast\Tagixo\Filament\Forms\FilamentFormColumns;
use Ccast\Tagixo\Models\FormSchema;

public function table(Table $table): Table
{
    $form = FormSchema::where('key', 'contact')->firstOrFail();

    return $table
        ->columns([
            TextColumn::make('created_at')->sortable(),
            ...FilamentFormColumns::from($form),
        ]);
}

Caching note: FilamentFormColumns::from() queries the database. For high-traffic panels, cache the FormSchema model or the resulting column array with a short TTL.


FilamentFormFilters

FilamentFormFilters::from(FormSchema $form) returns an array of Filament Filter instances for field types that support meaningful filtering.

Builder-type → Filter mapping

Builder field type Filament filter class
text Filter (text search)
email Filter (text search)
select SelectFilter
checkbox TernaryFilter
toggle TernaryFilter
date Filter (date range)
radio SelectFilter

Field types textarea, number, url, tel, and file are excluded from auto-filter generation because they rarely benefit from table-level filtering.

Usage

use Ccast\Tagixo\Filament\Forms\FilamentFormFilters;

return $table
    ->filters([
        ...FilamentFormFilters::from($form),
    ]);

Coherence Rules

The builder enforces the following constraints so that generated columns and filters remain consistent:

  • Sortable: text, email, number, date, select, radio columns are sortable. textarea, file, checkbox, toggle are not.
  • Searchable: text, email, url, tel columns are included in global table search. Others are not.
  • Filterable: Only the types listed in the filter mapping above generate a filter entry.

If you need to override these defaults (e.g. make a textarea sortable), do so after spreading the generated array rather than modifying the generator.


Complete Working Example

<?php

namespace App\Filament\Resources;

use App\Filament\Resources\FormSubmissionsResource\Pages;
use Ccast\Tagixo\Filament\Forms\FilamentFormColumns;
use Ccast\Tagixo\Filament\Forms\FilamentFormFilters;
use Ccast\Tagixo\Models\FormSchema;
use Ccast\Tagixo\Models\FormSubmission;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;

class FormSubmissionsResource extends Resource
{
    protected static ?string $model = FormSubmission::class;

    protected static ?string $navigationIcon = 'heroicon-o-inbox';

    public static function table(Table $table): Table
    {
        $form = FormSchema::where('key', 'contact')->firstOrFail();

        return $table
            ->columns([
                TextColumn::make('id')->sortable(),
                TextColumn::make('created_at')
                    ->label('Submitted')
                    ->dateTime()
                    ->sortable(),
                ...FilamentFormColumns::from($form),
            ])
            ->filters([
                ...FilamentFormFilters::from($form),
            ])
            ->defaultSort('created_at', 'desc');
    }

    public static function getPages(): array
    {
        return [
            'index' => Pages\ListFormSubmissions::route('/'),
        ];
    }
}

Companion service provider (render hook)

<?php

namespace App\Providers;

use Ccast\Tagixo\Filament\Forms\FilamentFormStyles;
use Filament\Support\Facades\FilamentView;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        FilamentView::registerRenderHook(
            'panels::body.start',
            fn () => FilamentFormStyles::scriptForForm('contact'),
        );
    }
}

Custom Column Overrides

Replace a specific column in-place

Use array_map to swap out the generated column for a field named message:

$columns = FilamentFormColumns::from($form);

$columns = array_map(function ($column) {
    if ($column->getName() === 'message') {
        return TextColumn::make('message')
            ->limit(80)
            ->tooltip(fn ($record) => $record->data['message'] ?? '');
    }
    return $column;
}, $columns);

Exclude a field and add a manual definition

Use array_filter to drop a field, then merge your own column:

$columns = array_filter(
    FilamentFormColumns::from($form),
    fn ($column) => $column->getName() !== 'attachment',
);

return $table->columns([
    ...array_values($columns),
    TextColumn::make('attachment')
        ->label('File')
        ->formatStateUsing(fn ($state) => basename($state ?? ''))
        ->url(fn ($record) => asset('storage/' . ($record->data['attachment'] ?? ''))),
]);