demo D98
Streaming in htmx
Rows arriving over time, first paint on the first chunk
source: { mode: 'stream' }
This grid takes rows that arrive over time from a websocket or a chunked response, painting the first chunk immediately rather than waiting for the last. It keeps a grid responsive on data that has no single payload, showing rows as they land.
This is the htmx version. A server-rendered table is upgraded in place, so the page is readable before any script runs and gains the full grid when the module loads, which suits a progressively enhanced app.
The configuration
<link rel="stylesheet" href="https://cdn.latticegrid.dev/lattice-grid@1.107.0/lattice-grid.min.css">
<!-- A real, server-rendered table: readable with JavaScript off. data-field on
each header names the column that cell becomes, and data-id on each row is
the key the grid carries it under. -->
<table id="streaming-source-table" data-lattice-grid>
<thead>
<tr>
<th data-field="symbol">Symbol</th>
<th data-field="name">Instrument</th>
<th data-field="desk">Desk</th>
<th data-field="region">Region</th>
<th data-field="price">Price</th>
<th data-field="change">Change</th>
<th data-field="volume">Volume</th>
<th data-field="notional">Notional</th>
</tr>
</thead>
<tbody>
<tr data-id="INS-0000000"><td>ELMW.G0</td><td>QRTZ Holdings</td><td>Rates</td><td>Singapore</td><td>462.0258</td><td>0.0044</td><td>239420</td><td>110618217</td></tr>
<tr data-id="INS-0000001"><td>RVNS.A1</td><td>FRTH Holdings</td><td>Credit</td><td>Singapore</td><td>241.8711</td><td>0.0447</td><td>119353</td><td>28868041</td></tr>
<tr data-id="INS-0000002"><td>NRTH.E2</td><td>HRTL Partners</td><td>Commodities</td><td>London</td><td>98.9614</td><td>0.0297</td><td>160242</td><td>15857773</td></tr>
<!-- the rest of the rows, rendered by your server -->
</tbody>
</table>
<script type="module">
import { hydrateTable } from 'https://cdn.latticegrid.dev/lattice-grid@1.107.0/modules/htmx.esm.min.js';
// The markup carries the rows; a server-rendered table's column types are
// declared in the config hydrateTable takes. The grid replaces the table
// where it stood.
const grid = hydrateTable(document.getElementById('streaming-source-table'), {
selection: 'multiple',
toolPanel: {
side: 'left',
panels: ['columns', 'filters', 'views', 'quick'],
actions: ['undo', 'redo', 'export', 'restore', 'maximise'],
exportName: 'lattice-demo',
},
statusBar: { panels: ['rowCount', 'progress', 'updates', 'selectedCount'] },
// The first chunk paints immediately and the rest merge in behind it, so
// sorting part-way through the load keeps working: arriving rows are
// merge-inserted, not re-sorted. open returns an async iterable of chunks,
// each { rows, progress?, done? }; the last chunk carries done: true.
source: {
mode: 'stream',
open: (req) => openStream({ signal: req.signal }), // your server feed of chunks
},
columns: [
{ field: 'symbol', title: 'Symbol', layout: { width: 130, pin: 'start' } },
{ field: 'name', title: 'Instrument', layout: { flex: 1, min: 170, max: 280 } },
{ field: 'desk', title: 'Desk', filter: { type: 'set' } },
{ field: 'region', title: 'Region', filter: { type: 'set' } },
{ field: 'price', title: 'Price', type: 'number', layout: { width: 120 },
format: { decimals: 4 }, filter: { type: 'number' } },
{ field: 'change', title: 'Change', type: 'number', layout: { width: 110 },
format: { style: 'percent', decimals: 2 } },
{ field: 'volume', title: 'Volume', type: 'number', layout: { width: 120 },
format: { notation: 'compact' }, filter: { type: 'number' } },
{ field: 'notional', title: 'Notional', type: 'number', layout: { width: 150 },
format: { style: 'currency', currency: 'USD', notation: 'compact' } },
{ field: 'status', title: 'Status', filter: { type: 'set' }, layout: { width: 110 },
cell: { decoration: 'pill', variant: { map: { open: 'success', halted: 'danger', settled: 'neutral' } } } },
],
// No rows: they arrive chunk by chunk from the stream.
});
</script>
Rendering rows as they arrive, without waiting for the full response
A streaming source is for data that does not exist as a single payload: a websocket feed, a chunked HTTP response, a backend that resolves rows in batches rather than all at once. Rather than blocking the grid until the last row lands, Lattice Grid paints the first chunk the moment it arrives and appends each subsequent chunk to the existing render, so the 120,000-row feed on this page, arriving in chunks of 2,000, produces a usable grid within the time it takes the first chunk to arrive. The mode is set with source: { mode: 'stream' }, and from that point the grid treats incoming rows as an open-ended sequence rather than a fixed table, which matters for anything built around a server-side row model where the client never holds a definitive row count up front. Because the grid is a JavaScript data grid working directly against the DOM rather than a virtual document layer, each batch is appended without a full reflow of rows already on screen, so scroll position and any active selection stay put as new rows land underneath. This suits dashboards, log viewers and trading tickets: any surface where the value of a row is highest the moment it is available, and where waiting for completeness would mean showing nothing during the interval that matters most.
How do I show grid rows as they stream in rather than waiting for all the data?
Set source: { mode: 'stream' } on the grid configuration. Lattice Grid then renders each batch of rows as it is received instead of buffering until the source signals completion, so the first paint happens on the first chunk. Existing rows, scroll position and selection are preserved as later batches are appended.