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 theFormSchemamodel 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,radiocolumns are sortable.textarea,file,checkbox,toggleare not. - Searchable:
text,email,url,telcolumns 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'] ?? ''))),
]);