Let Destructive Edits Be Destructive; Undo Is the Recovery Mechanism
TLDR
Storing hidden restore metadata inside cell attributes (likedata-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 Model | Storage Mechanism | Export Cleanliness | State Synchronization |
|---|---|---|---|
| Per-Cell Hidden Stash | data-absorbed attributes | Leaks stashed data to HTML | Conflicts with global undo |
| Global History Stack | Bracketed DOM Snapshots | 100% Clean HTML Output | Single 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:
- Hidden dataset attributes leak into exported HTML unless explicitly stripped.
- Stashed state can conflict with global undo stack histories.
- 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.