Elyra
Elyra The coding agent eTerm The terminal that knows where each command ends Huskarl AI colleagues on your own Mac Starf An activity monitor for Apple silicon that never invents a number etrans An SSH and SFTP client for macOS Elyra VM Virtual machines for macOS, Linux and Windows on your Mac Litr A small, native web browser for macOS Notr A notebook for macOS e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Refr Local-first PDF workspace for macOS Elyra Workspace A desktop workspace for coding agents Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Elyra
Livewire + Flux UI client

Livewire + Flux UI client

The Livewire client is server-driven: grid state lives in the component, and the protocol request is built and answered on the server during render. It uses Flux UI for chrome and inlines the theme automatically.

Install

composer require elyra/datagrid-livewire
php artisan vendor:publish --tag=elyra-datagrid-lang   # optional (translations)

Requires Livewire 4.3.4+ and Flux UI 2.12.2+, on Laravel 13.30+ and PHP 8.3+.

Anatomy

Extend the abstract DataGrid component. Implement definition() (server source) and columns() (display config); optionally aggregates().

<?php

namespace App\Livewire;

use Elyra\DataGrid\Livewire\DataGrid;
use Elyra\DataGrid\Server\Grid;
use Elyra\DataGrid\Server\GridDefinition;
use Livewire\Attributes\Layout;

#[Layout('components.layouts.app')]
class SalesGrid extends DataGrid
{
    public string $heading = 'Sales';

    // Features (toggle on/off)
    public bool $sortable    = true;
    public bool $filterable  = true;   // filter row + facet dropdowns
    public bool $searchable  = true;
    public bool $selectable  = true;
    public bool $groupable   = true;
    public bool $editable    = true;
    public string $editMode  = 'slideover';  // 'inline' | 'modal' | 'slideover'
    public bool $exportable  = true;
    public bool $masterDetail = false;
    public int $perPage      = 20;

    // Theme
    public string $accent = '';          // '' = follow app; or 'emerald', etc.

    protected function definition(): GridDefinition
    {
        return Grid::table('sales')->connection('elyrasql')->key('id')
            ->columns(['id', 'store_id', 'region', 'category', 'sku'])
            ->column('status', editable: true, editor: 'select',
                rules: 'required|in:pending,paid,shipped,delivered,returned')
            ->column('qty', type: 'number', editable: true, rules: 'required|integer|min:0')
            ->column('revenue', type: 'number', editable: true, rules: 'required|numeric')
            ->column('margin', type: 'number')
            ->searchable(['sku', 'category']);
    }

    protected function columns(): array
    {
        return [
            ['field' => 'sku', 'header' => 'SKU', 'frozen' => true, 'cell' => ['type' => 'link', 'href' => '/orders/{id}']],
            ['field' => 'region', 'header' => 'Region'],
            ['field' => 'category', 'header' => 'Category'],
            ['field' => 'status', 'header' => 'Status', 'editable' => true, 'editor' => 'select', 'options' => [
                ['value' => 'pending', 'label' => 'pending'],
                ['value' => 'paid', 'label' => 'paid'],
            ], 'badges' => ['pending' => 1, 'paid' => 2, 'shipped' => 3, 'delivered' => 4, 'returned' => 5]],
            ['field' => 'qty', 'header' => 'Qty', 'type' => 'number', 'editable' => true, 'editor' => 'number'],
            ['field' => 'revenue', 'header' => 'Revenue', 'type' => 'number', 'editable' => true, 'editor' => 'number', 'cell' => ['type' => 'currency', 'symbol' => 'kr ', 'decimals' => 2]],
            ['field' => 'margin', 'header' => 'Margin', 'type' => 'number', 'cell' => ['type' => 'bar', 'max' => 1200]],
        ];
    }

    protected function aggregates(): array
    {
        return [
            ['field' => 'revenue', 'fn' => 'sum', 'as' => 'sum_revenue'],
            ['field' => 'margin', 'fn' => 'sum', 'as' => 'sum_margin'],
        ];
    }
}
// routes/web.php
Route::get('/sales', App\Livewire\SalesGrid::class);

Feature properties

Property Default Purpose
$sortable true Column sorting
$filterable true Filter row + operator menus
$filterRow true Show the discreet filter row
$searchable false Global search box
$selectable false Row selection + checkbox column
$groupable false Grouping + drag-to-group
$editable false Editing + command column
$editMode inline inline | modal | slideover
$filterBuilder false Advanced AND/OR filter builder
$assistant false Natural-language "ask the grid" box (needs laravel/ai and ELYRA_DATAGRID_ASSISTANT=true)
$exportable true Excel toolbar button
$exportFormat csv csv | xlsx
$masterDetail false Expandable detail rows
$detailView null Blade view for the detail row
$tree false Tree grid: ['parentField'=>…,'rootValue'=>null,'hasChildrenField'=>null]
$headerGroups [] Multi-column headers
$perPage 20 Page size
$pageSizeOptions [20,50,100,200] Page-size choices
$theme '' '' (app) or a preset like tokyo-night
$accent '' '' (app) or a Flux-style accent

The browser cannot change these

Set them as a subclass default or a mount attribute — both run on the server:

class SalesGrid extends DataGrid
{
    public bool $exportable = false;
}
<livewire:sales-grid :assistant="true" />

Every public Livewire property is writable from the browser ($wire.set(), or a hand-built update payload) unless something refuses it. The grid refuses writes to its own configuration, so public bool $exportable = false; holds even for a user who flips it in the console. Before 0.8.0 it did not: on a grid that relied on the route for authorization (withoutAuthorization()), that one call was enough to download the table.

The browser may write only what the views bind — the search box, page, sort, page size, the column filters, the filter builder, the ask box and the edit form. Properties you add to your subclass are not touched. To bind one of the package's own properties from a custom view, open it explicitly:

protected function clientWritableProperties(): array
{
    return [...parent::clientWritableProperties(), 'groupDetailRows'];
}

This is not #[Locked] on purpose: PHP does not inherit attributes onto a redeclared property, so a #[Locked] on the package's $exportable would stop applying the moment you wrote public bool $exportable = false; — exactly when it matters. The guard is a Livewire trait hook instead, so defining your own updating() does not switch it off either.

Natural-language filtering

Set public bool $assistant = true; to add an "ask in plain language" box to the toolbar. Typing a question calls GridDefinition::ask() server-side, which translates it into an allow-listed filter and applies it to the advanced builder. Requires the optional laravel/ai package and a provider key; degrades to a graceful message otherwise.

Two gates, both enforced server-side rather than by hiding the box: this component flag, and ELYRA_DATAGRID_ASSISTANT=true. Questions are also length- bounded and rate-limited per caller, because each one is a billed LLM call. See Natural-language filtering.

Notes

  • The theme CSS is inlined once per page via @include('elyra-datagrid::styles') inside the component view — no build step needed.
  • Editing, facets, grouping and paging all call the server package directly; totals/aggregates are cached per filter within the component lifecycle.
  • Keyboard navigation & cell selection are provided via Alpine (bundled with Livewire/Flux).

See the guides for editing, grouping, filtering, and theming.

Pivot

For a cross-tab over the same source — dimensions on two axes, with subtotals and margins — this client also ships Elyra\DataGrid\Livewire\DataPivot. See Pivot.