Engineering Journal
Table Formatter
Table Formatter

Let Destructive Edits Be Destructive; Undo Is the Recovery Mechanism

2026-07-16

TLDR

Storing hidden restore metadata inside cell attributes (like data-absorbed-content) creates parallel state mechanisms that conflict with global undo stacks. Relying on bracketed history snapshots makes destructive edits simple and predictable while preserving clean exports.
State Recovery ModelStorage MechanismExport CleanlinessState Synchronization
Per-Cell Hidden Stashdata-absorbed attributesLeaks stashed data to HTMLConflicts with global undo
Global History StackBracketed DOM Snapshots100% Clean HTML OutputSingle Source of Truth

Localizing backup state in DOM attributes leaks metadata and creates conflicting state channels

When merging adjacent spreadsheet cells, absorbed cell contents are overwritten. A common pattern involves stashing deleted values inside custom dataset attributes (data-absorbed="value") to allow un-merging later.

However, maintaining per-cell restore stashes creates several issues:

  1. Hidden dataset attributes leak into exported HTML unless explicitly stripped.
  2. Stashed state can conflict with global undo stack histories.
  3. Managing per-cell stashes adds code complexity across edit operations.

Bracketing commands with snapshot captures keeps markup clean and delegates state recovery

We removed custom restore attributes, relying on global history snapshots (saveCurrentState()) to handle edit reversals:

   SIDE-CHANNEL STATE RECOVERY (DIRTY):
   ┌──────────────────────────────────────────────┐
   │ <td> [data-absorbed-content="Stashed Value"] │ ──> Leaks memory & pollutes exports
   └──────────────────────────────────────────────┘
          ▲
          └─ Reversible local action (unmerge reads local attribute)

DELEGATED STATE RECOVERY (CLEAN): [Merge Command] │ ├─► 1. saveState() ──► [History Stack: [S0: Clean DOM (unmerged)]] ├─► 2. Mutate DOM ──► Overwrite cell content (No dataset tags!) └─► 3. saveState() ──► [History Stack: [S0, S1: Clean DOM (merged)]] │ ▼ (Undo restores clean S0!)

Here is the code block illustrating clean cell absorption:

// Perform clean, destructive cell span absorption bracketed by history snapshots
export function mergeSelectedCells(anchorCell, targetCells) {
  // 1. Snapshot state prior to mutation
  window.saveCurrentState();

// 2. Remove absorbed cells cleanly without stashing attributes targetCells.forEach(cell => cell.remove()); anchorCell.setAttribute('colspan', targetCells.length + 1);

// 3. Snapshot state after mutation window.saveCurrentState(); }

Relying on the global undo stack keeps cell elements clean and eliminates state synchronization bugs.

Rule of thumb: Delegate component rollback to a centralized history timeline instead of caching restoration states in localized DOM data properties.
Read this post in the full Engineering Journal →