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
Master-detail & tree grid

Master-detail & tree grid

Two ways to show hierarchy: master-detail (an expandable panel under a row) and a tree grid (self-referencing parent/child data with lazy children).

Master-detail

Expand a row to reveal a detail panel — a custom view, or a nested grid.

Livewire

class OrdersGrid extends DataGrid
{
    public bool $masterDetail = true;
    public ?string $detailView = 'orders.detail';  // Blade view, rendered with ['row' => ...]
    // ...
}

The grid adds an expand column; expanding renders your detailView (or a key–value fallback if none is set). Because the detail view is arbitrary Blade, it can contain another grid.

Vue / Svelte (core)

The core exposes the mechanism; the detail payload is loaded lazily:

const options = {
  /* ... */
  masterDetail: { load: (row) => fetch(`/api/orders/${row.id}/lines`).then(r => r.json()) },
};

grid.toggleExpand(rowId);
grid.isExpanded(rowId);
grid.detailOf(rowId);   // the loaded payload

Tree grid

Bind self-referencing data (a parent_id column). Root rows load first; expanding a node lazily fetches its children (WHERE parent = id).

const options = {
  key: 'id',
  columns: [
    { field: 'name', header: 'Name' },     // first column shows the tree
    { field: 'kind', header: 'Kind' },
    { field: 'headcount', header: 'Headcount', type: 'number', align: 'right' },
  ],
  fetch: laravelFetch('/api/org/grid'),
  initialData: props.initial,             // server-seeded roots
  tree: {
    parentField: 'parent_id',
    rootValue: 0,                          // value marking roots (default null)
    hasChildrenField: 'has_children',      // boolean column → show a chevron
    childLimit: 1000,                      // max children fetched per node (default 1000)
  },
};
  • The first column renders indentation + an expand chevron.
  • Children are fetched on demand and inserted after their parent with the next depth.
  • Roots can be server-seeded via initialData for an instant first paint.

Scaling. Tree mode issues one query per expanded node (plus one for the roots), and childLimit bounds a single level — it does not paginate within a level. It is built for modest fan-out (org charts, category trees), not for expanding thousands of nodes with thousands of children each. For very large hierarchies, prefer grouping or a filtered flat view. Raise childLimit only up to the protocol cap (1000); beyond that, redesign the view.

await grid.toggleTreeNode(nodeId);
grid.isTreeExpanded(nodeId);

Livewire supports the tree grid too — set the $tree property (the server component flattens the visible tree, one query per expanded node):

class OrgGrid extends DataGrid
{
    public bool|array $tree = [
        'parentField' => 'parent_id',
        'rootValue' => 0,
        'hasChildrenField' => 'has_children',
        'childLimit' => 1000, // max children fetched per node (default 1000)
    ];
    // definition() declaring parent_id (+ has_children) as columns
}

Server

No special server support is needed — children are just a filtered request:

function orgGrid()
{
    return Grid::table('org')->connection('elyrasql')->key('id')
        ->columns(['id', 'name', 'kind', 'headcount', 'has_children', 'parent_id'])
        ->column('budget', type: 'number');
}

// roots (server-seeded)
Route::get('/org', fn () => Inertia::render('Org', [
    'initial' => orgGrid()->respond(GridRequest::fromArray([
        'columns' => ['id', 'name', 'kind', 'headcount', 'has_children'],
        'filter' => ['logic' => 'and', 'filters' => [['field' => 'parent_id', 'op' => 'eq', 'value' => 0]]],
        'view' => ['skip' => 0, 'take' => 50],
    ])),
]));

// children (lazy)
Route::post('/api/org/grid', fn (Request $r) => orgGrid()->respond(GridRequest::fromArray($r->all())));

Declare parent_id (and has_children) as columns so the client can filter on them, even if you do not display them.