All notes
EngineeringAugust 6, 20263 min

The editor that ate my <

Here's a bug that felt like gaslighting. You'd open SendDock's template editor, switch to the Code tab, and type an opening angle bracket — a lone <. The instant you typed it, it turned into &lt;. Type <div> and you'd watch it smear into &lt;div&gt; one character at a time, as if something were transcribing your HTML into entities in real time. Which, it turned out, is exactly what was happening.

Two editors, one value

The template editor has two tabs over the same content: a Code tab backed by CodeMirror, and a Visual tab backed by GrapesJS, the drag-and-drop builder. Both are bound to the same v-model — the template HTML. Type in one, the other reflects it. That shared value is the whole point, and also the whole problem.

The Visual tab was hidden with v-show. And v-show doesn't unmount — it just sets display: none. So while you were happily typing in the Code tab, the GrapesJS instance on the invisible Visual tab was still mounted, still alive, still watching.

The feedback loop

Here's the sequence, per keystroke:

  • You type < in CodeMirror. The shared v-model becomes the string <.
  • GrapesJS, watching that model, fires its watcher and calls setComponents() with the in-progress value.
  • < on its own is not valid HTML. GrapesJS does something reasonable for a parser handed junk: it treats the lone < as text content. And text content, serialized safely back to HTML, is &lt;.
  • GrapesJS re-emits &lt; into the shared v-model.
  • CodeMirror, bound to that same model, now shows &lt;.

Every keystroke round-tripped through a hidden HTML parser that "helpfully" escaped whatever wasn't yet valid markup. You weren't typing into an editor. You were typing into two editors playing telephone, and one of them didn't speak half-finished HTML.

The fix is one letter

v-show became v-if.

With v-if, the Visual tab's GrapesJS instance isn't mounted at all unless you're actually on that tab. No hidden component, no watcher, no round-trip, no escaping. The Code tab is just a code editor again.

That's the entire fix. One letter — which is the annoying and instructive thing about it. The bug wasn't in the code that looked broken (CodeMirror was doing its job perfectly) but in a component two tabs away that everyone had mentally filed as "not on screen, not running." v-show quietly violated that assumption.

The lesson

"Hidden" is not "gone." v-show, opacity: 0, visibility: hidden, an off-screen route kept alive for speed — all of them leave the component running, with its watchers, timers, and subscriptions fully live. If two components share a source of truth and one of them transforms that truth on change, it does not matter that it's invisible. Invisible code still runs. And when it runs against a value another component is actively editing, you get a bug that types back at you.

When something on screen is behaving impossibly, the culprit is often something off screen that never stopped working.

Related notes