Lattice Grid Buy a licence

demo D237

Editing a remote source, safely

Optimistic edit, reconcile to server truth, and a visible revert when the write is refused

createPushdownSource({ adapter, edit: true })

Building…
Loading a live grid…

The configuration

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.28.0/lattice-grid.min.css">

<script type="module">
  import { defineLatticeGrid } from 'https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.28.0/modules/webcomponent.esm.js';
  defineLatticeGrid();
</script>

<lattice-grid row-key="id" style="display: block; height: 520px"></lattice-grid>

<script type="module">
  import { createPushdownSource } from 'https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.28.0/modules/webcomponent.esm.js';

  // Any adapter can accept writes: declare mutate in its capabilities and add a
  // mutate(op) method. createPushdownSource then synthesises the edit.commit that
  // talks to it. Here a Map stands in for a remote table; a real adapter wraps a
  // genuine connection the same way.
  const store = new Map([
    ['1', { id: '1', part: 'Bushing A', line: 'Line 1', bore: 10.0, qty: 120 }],
    ['2', { id: '2', part: 'Bushing B', line: 'Line 1', bore: 12.5, qty: 80 }],
    ['3', { id: '3', part: 'Collar C', line: 'Line 2', bore: 8.25, qty: 200 }],
    // e.g. one object per row, keyed by id
  ]);

  const adapter = {
    name: 'mock-remote',
    capabilities: { filter: 'tree', sort: true, mutate: { update: true, returning: 'row' } },
    async execute() {
      const rows = [...store.values()].map((r) => ({ ...r }));
      return { rows, total: rows.length };
    },
    async mutate(op) {
      if (op.kind !== 'update') throw new Error('unsupported mutation: ' + op.kind);
      const row = store.get(op.key);
      if (!row) throw new Error('no such row: ' + op.key);
      Object.assign(row, op.patch);
      // The server is free to normalise the value; returning the row is what lets
      // the grid reconcile the cell to server truth.
      if ('bore' in op.patch) row.bore = Math.round(Number(op.patch.bore) * 10) / 10;
      return { ok: true, rows: [{ ...row }] };
    },
  };

  // The adapter answers filter and sort itself, so nothing is left to finish in
  // the browser and no compute engine is needed here. edit: true is what asks
  // the source to build the write-back commit.
  const source = createPushdownSource({ adapter, edit: true });

  // createPushdownSource only builds the commit; source.edit is the { commit }
  // the grid's edit option expects, so it is handed across as-is.
  const el = document.querySelector('lattice-grid');
  el.config = {
    edit: source.edit,
    toolPanel: { side: 'left', panels: ['columns'] },
    columns: [
      { field: 'part', title: 'Part', layout: { width: 140, pin: 'start' }, edit: { enabled: true, editor: 'text' } },
      { field: 'line', title: 'Line', layout: { width: 100 } },
      { field: 'bore', title: 'Bore', type: 'millimetres', format: { decimals: 2 }, layout: { width: 120 }, edit: { enabled: true, editor: 'number' } },
      { field: 'qty', title: 'Qty', type: 'number', layout: { width: 100 }, edit: { enabled: true, editor: 'number' } },
    ],
    source,
  };

  // An edit applies to the cell at once, then settles: confirmed to server truth,
  // reverted on a refusal, or surfaced as a conflict when the row had moved.
  // el.grid is the live grid inside the element.
  el.grid.on('cell:pending', (e) => console.log('pending', e.colId, e.key, e.value));
  el.grid.on('cell:confirmed', (e) => console.log('confirmed', e.colId, e.key, e.value));
  el.grid.on('cell:reverted', (e) => console.log('reverted', e.colId, e.key, e.reason));
  el.grid.on('cell:conflict', (e) => console.log('conflict', e.key, e.value));
</script>

Editing a remote source without waiting for the round trip, safely

A pushdown source is read-only by default: an adapter says nothing about writing, so the grid refuses to try. Editing one is an adapter opt-in, not a grid-level switch. An adapter declares capabilities.mutate: { update: true, returning: 'row' } and implements a mutate(op) method that persists one change, and createPushdownSource({ adapter, edit: true }) synthesises the edit.commit that wires a cell edit to it, the same bridge every pushdown adapter, including the DuckDB and OData ones this site’s other demos use, rides underneath. The sequence a host actually sees is: a cell edit applies to the grid immediately and fires cell:pending, so typing never waits on the network; the adapter’s mutate call resolves and, when it returns the authoritative row (returning: 'row'), the cell reconciles to whatever the server actually stored and fires cell:confirmed; a rejected write reverts the cell visibly to its prior value and fires cell:reverted with the reason the adapter gave, rather than leaving a value on screen that was never saved; and a write that discovers the row moved underneath it surfaces cell:conflict without dropping the edit, last-write-wins with the divergence named rather than swallowed. A newer edit to the same cell supersedes an older one still in flight, and the older write’s stale result is never allowed to land back over it.

How do I make a remote or pushdown data source editable?

Declare mutate: { update: true, returning: 'row' | 'none' } on the adapter’s capabilities, and implement a mutate(op) method that receives { kind: 'update', key, patch } and persists it. Pass edit: true to createPushdownSource({ adapter, edit: true }) and the grid gains a working edit.commit wired to that method automatically; a source whose adapter declares nothing about mutation stays read-only and a write to it is refused rather than silently dropped.

What happens if two people edit the same remote row at once?

The later edit wins and is the one sent to the server. If the adapter’s response indicates the row had already diverged, the grid fires cell:conflict alongside the normal confirmation, carrying the server’s row so the interface can show the divergence, rather than either silently overwriting it or blocking the edit that already applied.