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 <. Type <div> and you'd watch it
smear into <div> 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 sharedv-modelbecomes 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<.- GrapesJS re-emits
<into the sharedv-model. - CodeMirror, bound to that same model, now shows
<.
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
The server that restarted every five hours
A container that crash-looped with no panic and no error log — four times, for four different reasons. A tour through every way a health check can lie to you.
Make the broadcast survive a restart
The first version sent your whole campaign from a single goroutine — and lost everything if the process blinked. Rebuilding it as a durable per-recipient queue.