Form Builder

Form Tables: Columns and Filters

Surface form submissions in Primix tables using PrimixFormColumns, PrimixFormFilters, and PrimixFormStyles.

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., @script Blade 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() queries tgx_forms on every call. In high-traffic panels, wrap the call in Cache::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 DateFilter class with its own date-range picker. Do not use the DatePicker workaround sometimes seen in Filament-based code — that class is not present in Primix and will throw a ClassNotFoundException at 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.