Custom columns
This walkthrough adds a StatusBadge column that renders a coloured pill based on a row’s status value.
1. Publish the JS scaffold
Section titled “1. Publish the JS scaffold”If you have not done this yet:
php artisan vendor:publish --tag=lattice-jsThis writes a single resources/js/registry.ts if it does not already exist — the one place custom fields, components, and columns are registered.
2. Generate the column
Section titled “2. Generate the column”php artisan lattice:column StatusBadgeThis creates:
app/Tables/Columns/StatusBadge.php— the PHP column class.resources/js/columns/status-badge.tsx— the React cell renderer stub.- An entry under
columnsinresources/js/registry.tswiring them together. - Runs
lattice:typescriptto refresh the generated types file.
The PHP attribute receives the short identifier status-badge; the wire type is column.status-badge.
Pass --type= to override it.
3. The generated PHP class
Section titled “3. The generated PHP class”A column reflects its public properties into its wire props — exactly like a component — so there
is no toData() and no separate props class to maintain.
<?php
namespace App\Tables\Columns;
use Lattice\Lattice\Tables\Attributes\AsColumn;use Lattice\Lattice\Tables\Columns\Column;
#[AsColumn(type: 'status-badge')]class StatusBadge extends Column{ // Add public properties here; they are reflected into the column's // TypeScript props and passed to the cell renderer. Keep internal state // (filter flags, helpers) protected so it stays off the wire.}Add a public property for anything the cell renderer needs at render time, and a fluent setter for it. For example, a colour map that controls which status gets which colour:
#[AsColumn(type: 'status-badge')]class StatusBadge extends Column{ /** @var array<string, string>|null */ public ?array $colorMap = null;
/** @param array<string, string> $colorMap */ public function colorMap(array $colorMap): static { $this->colorMap = $colorMap === [] ? null : $colorMap;
return $this; }}colorMap is public, so it lands in the column’s props; declaring it nullable keeps the wire shape
honest (it is null until set). Internal state a cell never reads — filter flags, cached lookups —
stays protected so reflection leaves it off the wire.
4. The generated React cell renderer
Section titled “4. The generated React cell renderer”import type { ColumnCellComponent } from "@lattice-php/lattice";
export const StatusBadgeCell: ColumnCellComponent = ({ value }) => { return <span>{String(value ?? "")}</span>;};A ColumnCellComponent receives { column, props, row, value }:
value— the raw cell value (the column’s key resolved from the row).row— the full row data object.props— the column’s props sent from PHP. They are a loose bag by default; type the cell asColumnCellComponent<"column.status-badge">to narrow them to your column’s props (see below).column— the full serialized column descriptor (key, label, type, nested columns).
Replace the stub body with real UI:
import type { ColumnCellComponent } from "@lattice-php/lattice";
const colorClasses: Record<string, string> = { active: "bg-green-100 text-green-800", archived: "bg-red-100 text-red-800", draft: "bg-gray-100 text-gray-800",};
export const StatusBadgeCell: ColumnCellComponent = ({ props, value }) => { const label = String(value ?? ""); const map = (props?.colorMap as Record<string, string> | undefined) ?? colorClasses; const classes = map[label] ?? "bg-gray-100 text-gray-800";
return ( <span className={`inline-flex items-center rounded px-2 py-0.5 text-xs font-medium ${classes}`}> {label} </span> );};To drop the cast, type the cell as ColumnCellComponent<"column.status-badge">. After lattice:typescript, props is narrowed to your column’s generated props:
import type { ColumnCellComponent } from "@lattice-php/lattice";
export const StatusBadgeCell: ColumnCellComponent<"column.status-badge"> = ({ props, value }) => { const map = props.colorMap ?? colorClasses; // typed, no cast // ...};The generator registers column cells bare ("column.status-badge": StatusBadgeCell). The optional columnCell() helper is a type-narrowing identity wrapper — columnCell(StatusBadgeCell) — for when you want the registry entry itself type-checked against the column’s props; it changes nothing at runtime.
5. The registry entry
Section titled “5. The registry entry”The generator appended an entry under columns in resources/js/registry.ts:
import { createPlugin, extendRegistry, registry as packageRegistry } from "@lattice-php/lattice";import { StatusBadgeCell } from "./columns/status-badge";
export const registry = extendRegistry( packageRegistry, createPlugin({ name: "app", components: {}, columns: { "column.status-badge": StatusBadgeCell, }, }),);6. Wire the registry in app.tsx
Section titled “6. Wire the registry in app.tsx”registry.ts already merges your column onto the built-in registry, so import its exported registry and pass it to Provider:
import "../css/app.css";import { createInertiaApp } from "@inertiajs/react";import { createRoot } from "react-dom/client";import LatticePage from "@lattice-php/lattice/page";import { Provider } from "@lattice-php/lattice";import { registry } from "./registry";
createInertiaApp({ resolve: (name) => { if (name === "lattice/page") return { default: LatticePage }; const pages = import.meta.glob("./Pages/**/*.tsx", { eager: true }); return pages[`./Pages/${name}.tsx`]; }, setup({ el, App, props }) { if (!el) return; createRoot(el).render( <Provider registry={registry}> <App {...props} /> </Provider>, ); },});Custom fields, components, and columns all live in this same registry.ts — there is no second registry to merge.
7. Generate TypeScript types
Section titled “7. Generate TypeScript types”php artisan lattice:typescriptThis augments ColumnProps in @lattice-php/lattice:
declare module "@lattice-php/lattice" { interface ColumnProps { "column.status-badge": { colorMap: Record<string, string> | null; }; }}column.props is now narrowed in the cell renderer, eliminating the cast.
8. Use the column in a table
Section titled “8. Use the column in a table”use App\Tables\Columns\StatusBadge;use Illuminate\Database\Eloquent\Builder;use Lattice\Lattice\Attributes\AsTable;use Lattice\Lattice\Tables\Columns\Column;use Lattice\Lattice\Tables\Columns\TextColumn;use Lattice\Lattice\Tables\Sources\Eloquent\EloquentTableDefinition;use Lattice\Lattice\Tables\TableQuery;
/** * @extends EloquentTableDefinition<\App\Models\User> */#[AsTable('app.users')]final class UsersTable extends EloquentTableDefinition{ /** * @return array<int, Column> */ public function columns(): array { return [ TextColumn::make('name')->label('Name')->sortable(), StatusBadge::make('status')->label('Status') ->colorMap([ 'active' => 'bg-green-100 text-green-800', 'archived' => 'bg-red-100 text-red-800', 'draft' => 'bg-gray-100 text-gray-800', ]), ]; }
/** * @return Builder<\App\Models\User> */ public function builder(TableQuery $query): Builder { return \App\Models\User::query()->select(['id', 'name', 'status']); }}