DataGrid · · 6 min read

Your Data Grid Shouldn't Be the Scary Part

Building fast, friendly tables in Laravel — and why we let the database do the heavy lifting. Now with a first-class PostgreSQL driver in 0.4.0.

Your Data Grid Shouldn't Be the Scary Part

Building fast, friendly tables in Laravel — and why we let the database do the heavy lifting.

There's a particular kind of dread that sets in when a ticket says "just add a table." You know how it goes. It starts innocent — a few columns, some sorting. Then someone wants filters. Then facets with counts. Then "can we group by region and show subtotals?" Then export. Then it's ten million rows and the page takes nine seconds and everyone's sad.

We built Elyra DataGrid because we were tired of that arc. This is the warm, honest tour: why it works the way it does, and how you actually use it — with real snippets you can paste.

Grab a coffee. This won't take long.

The one idea that changes everything

Most grid libraries load your rows into memory and sort/filter/paginate them there. That's lovely for 200 rows and a disaster for 2 million.

Elyra DataGrid flips it: the database does the work. The grid describes what it wants as a small, well-defined request, and your database — the thing that has indexes and has spent decades getting good at exactly this — answers it.

Browser  ──GridRequest──▶  Server (allow-list + SQL)  ──▶  Database
        ◀──GridResponse──                             ◀──

One shared protocol (GridRequest / GridResponse), described once and understood by both the TypeScript clients and the PHP server. One headless engine. Three clients (Livewire, Vue, Svelte) that all wrap the same brain. You learn it once.

And a quiet principle we're proud of: cost is opt-in. Total row counts, facet counts, aggregates — none of it runs unless a feature on screen actually needs it. Totals get cached per filter. Facets load lazily when a dropdown opens. Your default table is cheap.

How: a grid in about a dozen lines

Here's the server side. This object is the security boundary — only declared columns can ever be sorted, filtered, or written:

use Elyra\DataGrid\Server\Grid;

$grid = Grid::table('sales')->key('id') ->columns(['sku', 'region', 'status']) ->column('revenue', type: 'number') ->searchable(['sku', 'region']);

Answering a request from your client is one line:

return $grid->respond(GridRequest::fromArray($request->all()));

That's it. Sorting, paging, and filtering are on by default. You didn't wire any of it.

On Livewire, it's a component

Extend DataGrid, describe your source and columns, flip a few flags:

class SalesGrid extends DataGrid
{
public bool $selectable = true;
public bool $groupable  = true;

protected function definition(): GridDefinition
{
    return Grid::table('sales')->key('id')
        ->columns(['sku', 'region', 'status'])
        ->column('revenue', type: 'number')
        ->searchable(['sku'])
        ->authorize(fn (string $ability) => auth()->check());
}

protected function columns(): array
{
    return [
        ['field' => 'sku',     'header' => 'SKU', 'frozen' => true],
        ['field' => 'region',  'header' => 'Region'],
        ['field' => 'revenue', 'header' => 'Revenue', 'type' => 'number', 'align' => 'right'],
    ];
}

}

<livewire:sales-grid />

Sortable headers, a search box, pagination, row selection, grouping — all there, all server-driven.

On Vue or Svelte, it's a headless engine

Same brain, no DOM opinions. You bring the fetch, you render however you like:

import { createGrid } from "@elyra/datagrid-client-core";

const grid = createGrid({ key: "id", columns: [ { field: "name", header: "Name" }, { field: "city", header: "City" }, { field: "amount", header: "Amount", type: "number" }, ], paging: { pageSize: 25 }, search: { mode: "substring", fields: ["name", "city"] }, fetch: (req) => api.post("/customers/grid", req).then((r) => r.data), });

grid.getState() gives you rows and metadata; grid.subscribe(render) re-renders on change; grid.toggleSort("name"), grid.setSearch("berlin"), grid.goToPage(2) do what they say. (The Vue and Svelte wrapper components are there too if you'd rather not render by hand.)

Turn features on as you need them

The core stays simple; the power is opt-in. A few favourites:

// Group by region with subtotals (ROLLUP under the hood)
public bool $groupable = true;

// Self-referencing tree data, children loaded lazily per node public bool|array $tree = ['parentField' => 'parent_id', 'childLimit' => 500];

// Inline + batch editing, with a clipboard-paste that writes in one transaction public bool $editable = true;

And when you want to feel like you're living in the future — ask the grid in plain language. The model's answer is re-validated against your allow-list and compiled through the same bound-parameter path as a hand-built filter, so it can only ever produce filters your grid already permits:

$filter = $grid->ask('paid orders over 5k in the nordics last quarter');

Off-target guess? It quietly drops the offending condition. Never unsafe SQL. Safe by design, not by hope.

New in 0.4.0: bring your own PostgreSQL 🐘

This is the part we're most excited to share. Elyra DataGrid is native to ElyraSQL, but it's genuinely portable — and as of 0.4.0 there's a first-class PostgreSQL driver:

Grid::table('users')->driver('pgsql')->connection('pgsql')
->columns(['name', 'email', 'country'])
->column('signups', type: 'number')
->searchable(['name', 'email']);

It's not a lowest-common-denominator port. It speaks Postgres:

  • Identifiers quoted the Postgres way ("col")

  • Subtotals via GROUP BY ROLLUP(…)

  • Real percentiles and medians with percentile_cont(p) WITHIN GROUP (ORDER BY …)

  • Genuine full-text search — search: { mode: 'fulltext' } compiles to to_tsvector(…) @@ plainto_tsquery(…)

And it's not just unit-tested for shape; there's a live integration test that runs the generated SQL against a real PostgreSQL server, so we know it actually parses and behaves. PostgreSQL joins MySQL, ClickHouse, and SQLite / Elyra SQL Anywhere — pick the one your app already uses, and everything degrades gracefully per dialect.

A word on safety, because it matters

Also new in 0.4.0, and worth saying plainly: writes and exports are now secure-by-default. A grid won't mutate(), mutateBatch(), or export() until you've told it who's allowed:

Grid::table('sales')->key('id')
->authorize(fn (string $ability) => auth()->user()?->can($ability, Sale::class) ?? false);
// ...or ->withoutAuthorization() when your route/policy already guards it

Forget to configure it and you get a loud exception before anything touches the database — not a silent open door. That's the kind of default we like: the safe path is the easy path.

Come on in

That's the whole pitch, really. Let the database do what it's good at. Share one protocol so your front end and back end never drift. Keep the common case cheap and the powerful case one flag away. And make the safe thing the default thing.

If you've been dreading "just add a table" — maybe you don't have to anymore.

composer require elyra/datagrid-server

Happy gridding. ☕