Form Tables: Columns, Filters, and CSS Injection
This document covers the three classes that bridge Tagixo's visual form builder with Primix's table system, and explains the CSS injection mechanism that keeps form styles alive through LiVue page morphs.
Overview
| Class | Purpose |
|---|---|
PrimixFormColumns |
Generates a Column[] array from a form schema slug |
PrimixFormFilters |
Generates a Filter[] array from a form schema slug |
PrimixFormStyles |
Injects per-form CSS into the Primix panel, surviving LiVue morphs |
All three classes accept a form slug as their primary contract. The slug must match the key stored in tgx_forms. Changing the slug in the builder breaks the mapping — treat it as a stable identifier.
The CSS Injection Problem
Primix admin pages are rendered by LiVue, which uses a virtual-DOM morphing strategy to update the page on navigation without a full reload. Styles injected via a plain <style> tag into <head> at render time are discarded on the next morph cycle because LiVue only reconciles elements it controls.
PrimixFormStyles works around this by injecting a <script> block instead of a <style> block. The script is idempotent: it checks whether a <style id="tgx-form-{slug}"> element already exists before inserting it, and replaces the content if the style has changed. Because <script> nodes are not touched by LiVue's morph, the injector re-runs on every navigation and ensures the style is always present.
Methods
| Method | Description |
|---|---|
scriptFrom(string $slug) |
Returns the full <script>…</script> string ready to embed in a render hook |
cssFrom(string $slug) |
Returns raw CSS for the form (without the injector wrapper) |
renderHook(string $slug) |
Registers the render hook internally; call from a service provider |
forResource(string $resourceClass) |
Derives the slug from the resource's getFormSlug() method and registers the hook |
Correct Render Hook
Use RESOURCE_PAGE_FORM_BEFORE (Primix) not BODY_START (Filament). The Filament hook fires before LiVue bootstraps, so the injector script runs once and is never re-triggered on client-side navigation.
LiVue vs. Livewire: Primix uses LiVue for its reactive layer, not Livewire. Livewire-centric patterns (e.g.,
@scriptBlade directives,wire:ignore) do not apply here and will either be ignored or cause conflicts.
PrimixFormColumns
PrimixFormColumns::fromSlug(string $slug) returns an array of Column objects built from the field definitions stored in tgx_forms for that slug.
Builder Type → Column Class Mapping
| Builder field type | Primix column class | Notes |
|---|---|---|
text |
TextColumn |
|
textarea |
TextColumn |
Truncated at 80 chars by default |
number |
NumericColumn |
|
email |
TextColumn |
Rendered as a mailto: link |
tel |
TextColumn |
|
url |
TextColumn |
Rendered as an external link |
date |
DateColumn |
|
datetime |
DateTimeColumn |
|
time |
TimeColumn |
|
select |
TextColumn |
Resolves option label from schema |
radio |
TextColumn |
Resolves option label from schema |
checkbox |
BooleanColumn |
|
toggle |
BooleanColumn |
|
file |
TextColumn |
Renders filename; no preview |
image |
ImageColumn |
Renders a thumbnail |
Usage
use Ccast\TagixoPrimix\FormTables\PrimixFormColumns;
public function table(Table $table): Table
{
return $table->columns([
...PrimixFormColumns::fromSlug('contact-form'),
]);
}
Performance:
fromSlug()queriestgx_formson every call. In high-traffic panels, wrap the call inCache::remember()with a short TTL (e.g. 60 seconds) or bust on form save.
PrimixFormFilters
PrimixFormFilters::fromSlug(string $slug) returns an array of Filter objects for fields that are marked as filterable in the form schema.
Filter Type Mapping
| Builder field type | Primix filter class | Notes |
|---|---|---|
text, textarea, email, tel, url |
TextFilter |
Case-insensitive LIKE |
number |
RangeFilter |
Min/max inputs |
date, datetime |
DateFilter |
Native Primix class |
select, radio |
SelectFilter |
Options pulled from schema |
checkbox, toggle |
BooleanFilter |
Primix-specific: Primix ships a native
DateFilterclass with its own date-range picker. Do not use theDatePickerworkaround sometimes seen in Filament-based code — that class is not present in Primix and will throw aClassNotFoundExceptionat runtime.
Coherence Rules
The builder enforces constraints on auto-generated objects to keep the output valid. These rules apply at generation time and cannot be overridden per-column without rebuilding the object manually.
| Constraint | Rule |
|---|---|
| Sortable | Only text, number, date, datetime, time columns are sortable |
| Searchable | Only text, email, tel, url, textarea columns are searchable |
| Copyable | Only text, email, tel, url columns expose the copy action |
| Filterable | Fields must have filterable: true in the schema; the builder sets this per-field |
| Hidden by default | file and image columns are hidden in the table index and must be explicitly shown |
To override a rule, replace the auto-generated column by name after spreading:
$columns = PrimixFormColumns::fromSlug('contact-form');
// Replace the auto-generated 'email' column with a custom one
$columns = array_map(function ($col) {
if ($col->getName() === 'email') {
return TextColumn::make('email')
->label('Email address')
->copyable()
->searchable()
->sortable();
}
return $col;
}, $columns);
Complete Working Example
Resource
<?php
namespace App\Primix\Resources;
use Ccast\TagixoPrimix\FormTables\PrimixFormColumns;
use Ccast\TagixoPrimix\FormTables\PrimixFormFilters;
use Primix\Primix\Resources\Resource;
use Primix\Primix\Resources\Pages;
use Primix\Tables\Table;
class FormSubmissionsResource extends Resource
{
protected static string $model = \Ccast\Tagixo\Models\FormSubmission::class;
protected static ?string $navigationIcon = 'heroicon-o-inbox';
protected static ?string $navigationLabel = 'Form Submissions';
public static function getFormSlug(): string
{
return 'contact-form';
}
public static function table(Table $table): Table
{
return $table
->columns([
...PrimixFormColumns::fromSlug(static::getFormSlug()),
])
->filters([
...PrimixFormFilters::fromSlug(static::getFormSlug()),
])
->defaultSort('created_at', 'desc');
}
public static function getPages(): array
{
return [
'index' => Pages\ListRecords::route('/'),
];
}
}
Service Provider Registration
<?php
namespace App\Providers;
use Ccast\TagixoPrimix\FormTables\PrimixFormStyles;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
PrimixFormStyles::forResource(\App\Primix\Resources\FormSubmissionsResource::class);
}
}
Primix vs. Filament
| Aspect | Primix | Filament |
|---|---|---|
| Namespace | Primix\Tables\Columns\*, Primix\Tables\Filters\* |
Filament\Tables\Columns\*, Filament\Tables\Filters\* |
| Reactive layer | LiVue (virtual DOM morph) | Livewire (server-driven DOM diff) |
| Render hook API | PrimixFormStyles::renderHook() with RESOURCE_PAGE_FORM_BEFORE |
FilamentView::registerRenderHook() with BODY_START or RESOURCE_PAGES_LIST_RECORDS_TABLE_BEFORE |
| Date filter | DateFilter (native, built-in) |
Filter::make()->form([DatePicker::make()]) workaround |
| Boolean column | BooleanColumn — renders a check/cross icon |
IconColumn::make()->boolean() — different class entirely |
Warning: Do not mix Primix and Filament adapters in a dual-panel setup. If your app registers both a Filament panel and a Primix panel, each panel must use its own namespace for columns, filters, and render hooks. Importing a Filament column class into a Primix resource (or vice versa) will produce a silent render failure — the column will appear in the schema but render as an empty cell.