Skip to content

Datagrid edit conflict

Two people edit the same row; the second save must not silently win. This recipe adds optimistic locking to the datagrid-edit-errors record layout: the record tbody carries data-version, the PATCH includes it, and a stale version answers 409 with the record re-rendered as a conflict presentation — the server’s current values in the cells (data-attention="error"), the fresh version, and an alert naming both values with Overwrite / Discard actions. No modal, no client merge UI: the row is the merge UI. Covered by the versioning policy.

Also known as: optimistic locking, concurrency control.

Edit the Price (double-click, or Enter on the cell) and commit any number — the demo simulates another user having already saved 20.00, so your first commit conflicts: their value lands in the cell, and the alert offers to overwrite with yours (succeeds — the version is now fresh) or discard.

NamePrice
Chai18.00
<!-- Inside the .hc-datagrid wrapper — without this template nothing
is editable and hc:datagridedit never fires: -->
<template data-datagrid-editor data-col="price">
<input class="hc-input" type="text" inputmode="decimal" aria-label="Price">
</template>
<tbody class="hc-datagrid__record" id="item-1" data-version="3"
data-hx-patch="/items/1"
data-hx-trigger="hc:datagridedit"
data-hx-vals="js:{ col: event.detail.col, value: event.detail.value,
version: event.target.closest('tbody').dataset.version }"
data-hx-disinherit="hx-vals"
data-hx-swap="outerHTML">
<tr class="hc-datagrid__row">
<td class="hc-datagrid__cell">Chai</td>
<td class="hc-datagrid__cell" data-numeric data-editable
data-col="price" data-value="18">18.00</td>
</tr>
</tbody>

hc:datagridedit bubbles from the edited cell, so event.target.closest('tbody') resolves the record and its current version rides along. Add 409 to the same one-time htmx:beforeSwap allowance the field-errors contract documents for 422.

PATCH /items/:id (col, value, version):

CaseResponse
version matches, accepted200 + the record — server formatting, incremented data-version
version matches, rejected422 — the datagrid-edit-errors branch (version kept)
stale version409 + the conflict presentation: their values in the cells + data-attention="error", the fresh data-version, and a role="alert" conflict row naming both values with an Overwrite button (static data-hx-vals re-submitting yours against the fresh version) and a Discard button (data-hx-get of the row)
unknown row / column404 — the standard error toast covers it

GET /items/:id answers the record plain — the Discard target. A second conflict on Overwrite just re-presents with newer values; overwrite is last-writer-wins by explicit consent.

Optimistic locking degrades with the editing itself; the no-JS path is the edit-conflict recipe’s full-form 409 page.

  • The conflict announces via role="alert"; both resolutions are real buttons inside the message cell.
  • data-attention="error" marks the row visually and the message names the values — never color alone.
  • After either resolution the record re-renders in place; keyboard users continue where they were.
  • datagrid-edit-errors — the 422 sibling this recipe extends with versions.
  • edit-conflict — the same 409 philosophy for whole forms.
  • sse-updates — live row refreshes reduce conflicts; this contract catches the race that slips through.