Engineering Journal
Table Formatter
Table Formatter

Let destructive edits be destructive; undo is the recovery mechanism

2026-07-16

TLDR

When a table cell stretches over its neighbors, their content is gone. I chose not to squirrel that content away in hidden attributes so a later un-stretch could resurrect it. If the user regrets the edit, undo restores everything at once. Per-operation restore state is a shadow undo system that grows without bound and disagrees with the real one.

What the industry does

Non-destructive editing is the prestige pattern. Photoshop adjustment layers, video editors that never touch source footage, CRDT editors where deletion is a tombstone rather than removal. The appeal is real: any single decision can be revisited later without unwinding everything after it.

Applied to a table editor, the pattern says: when a cell absorbs its neighbors, stash their content (a data-absorbed attribute, a side map keyed by cell), so un-merging restores what was there.

Why it fails for this problem class

The stashed state has to survive everything else the editor does. Export to HTML: do the hidden attributes ship, leaking deleted content into output? Copy-paste the cell: does the stash travel? Edit the stretched cell, then un-stretch: is restoring year-old neighbor content even correct anymore? Each question needs code, and each answer is a policy a user cannot see.

Worse, the editor already has a recovery mechanism. Every operation here is bracketed by history snapshots:

window.saveCurrentState();          // snapshot before
absorbedCells.forEach(el => el.remove());
anchor.setAttribute('colspan', newSpan);
window.saveCurrentState();          // snapshot after

A parallel restore stash is a second undo system with different semantics. Two sources of truth about the past will disagree, and the disagreement surfaces as the weirdest class of bug: history that lies.

The better approach

Make the destructive operation honestly destructive, cheap to trigger, and one undo away from reversal. The absorbed cells are removed. Their content is in the snapshot. Ctrl+Z is the restore path, and it is the same restore path as for every other edit, so it has no novel failure modes.

This also kept the feature small. The span-stretch gesture is one function that validates, removes, and re-attributes. The non-destructive variant needs that plus a stash schema, an export filter, and a restore-priority policy.

Evidence

The honest version: this is an argued position backed by a working implementation, not a benchmark. The destructive implementation passed its 7 gesture tests on the first run after the geometry was right. I did not build the non-destructive variant, so I cannot cite its bug count; I can only point at the three unresolved policy questions above, none of which the destructive version has to answer. Treat this as a hypothesis with one data point.

What you give up

Selective recovery. Undo is linear: restoring absorbed content from ten edits ago costs the nine edits after it. A real stash would allow "un-stretch this one cell, keep everything else." If your users routinely merge, work for an hour, and then want the merge back, linear undo genuinely punishes them.

Snapshot cost is also real. Full-state snapshots per operation are heavy on big documents; a stash is tiny by comparison.

When the common pattern is right

Non-destructive state earns its complexity when history cannot serve recovery: collaborative editors where a linear stack does not exist, unbounded sessions where snapshots blow memory, or domains like photo editing where "revisit one decision from an hour ago" is the core workflow rather than an edge case. A single-user table formatter with bounded sessions is none of those. Match the recovery mechanism to how far back users actually reach.
Read this post in the full Engineering Journal →