Overview
Most web editors that ship a WASM “brain” still use the browser’s
editable surface: a contenteditable root (or a thin
wrapper). Rust handles validation, transforms, collaboration, and
heavy parsing; the DOM handles caret, IME, and native selection.
This page details policies and loops that keep that split healthy. For general model/view theory see Editor → Architecture; for IME and clipboard specifics see the linked Wasm guides.
contenteditable + WASM hybrid
In the hybrid pattern, JavaScript (or TS) owns the DOM tree the user types into: event listeners, optional virtual layer, and applying patches back from Rust. WASM owns the canonical document (or op log) you trust for undo, collaboration, and export.
- Pros: IME, spellcheck integration, accessibility tree, and platform selection behavior come from the browser.
- Cons: Every browser mutates HTML slightly; you must reconcile continuously. Our Scenarios document real cross-browser differences.
Rust does not replace the editable surface here—it replaces fragile or expensive document logic you prefer to keep deterministic and testable in one language.
Source of truth
Pick one authority for committed text at any moment:
- Model-led: After each stable edit (often after
IME
compositionend), WASM applies an op; JS updates the DOM to match. Risk: fighting the browser if you patch mid-composition. - DOM-led during typing: While the user edits, the DOM is truth; you periodically or on commit parse into the Rust model. Risk: drift if parsing is lossy—tighten schema and tests.
A practical split: treat the DOM as truth during an IME composition session, then commit a single stable update into Rust (see IME & composition).
Two competing “truths” (full DOM string and full Rust tree updated independently every keystroke) causes races and duplicate characters—design the handoff explicitly.
Input events & order
Build a clear pipeline in your controller code (not implicitly across scattered handlers):
-
beforeinput— intercept or cancel certain edits when the platform allows; readgetTargetRanges()when available. -
input— DOM has changed; good place for debounced sync outside active composition if you use DOM-led snapshots. -
composition*— gate when Rust sees committed text vs preview. -
selectionchange— map selection to model coordinates; watch for async WASM updates that might invalidate ranges ( Selection & offsets).
Deeper event semantics: Editor → Input handling.
DOM ↔ model loop
Typical loops look like one of:
- Parse on commit: Read serialized subtree or diff the DOM → send structured ops to WASM → optional WASM response patches DOM if model rejects or normalizes.
- Op stream from JS: Translate
beforeinputinput types into small ops in JS; WASM applies and returns “render instructions” for minimal DOM updates. - Projection: WASM is authoritative; after each transaction, JS replaces or patches the editable subtree from a serialized view (more control, more diff cost).
Avoid shipping the entire HTML string across the boundary on every event unless profiling proves it is cheap enough ( JS ↔ WASM boundary).
Model/DOM theory: Editor → Model–DOM synchronization.
DOM shape & schema
Browsers insert <br>, empty blocks, and nested
spans differently. Your Rust schema should define what is valid; after paste or Enter, normalize either in JS
(fast DOM surgery) or WASM (shared rules with native clients).
- Run normalization after clipboard insert and before persisting a CRDT update if paste can introduce forbidden tags ( Clipboard & input routing).
- Keep one policy with your sanitizer to avoid XSS divergence ( Security & deployment).
Schema concepts: Editor → Model & schema, HTML mapping.
Non-editable islands
Mixing contenteditable=false nodes, mentions, math
placeholders, or custom widgets inside the editor surface is common.
Selection can collapse or jump when crossing boundaries—behavior
differs on Android vs desktop.
WASM doesn’t fix this: your view layer must define click targets, keyboard traps, and how selection maps into the Rust model (skip widgets vs editable text).
Related: Accessibility, site cases on non-editable regions.
When to call WASM
Practical rules of thumb:
- Do call for schema validation, transform apply, paste HTML parse, CRDT merge, export/import, expensive diff.
- Avoid calling on every
compositionupdateunless you measured the need for live preview in the model. - Batch rapid typing into frame-aligned or micro-task batches if you use DOM snapshots—never reorder relative to IME commit order.
Canvas / custom renderers
Some products render body text on Canvas/WebGL but still keep a
hidden contenteditable for IME. Others go fully custom;
that shifts IME and accessibility work onto you.
Prefer staying DOM-hosted until you have a concrete requirement (print-accurate pagination, game UI) that outweighs the cost.
Where Rust usually sits
- Document model + operations (apply transforms, enforce schema).
- Parsing / normalization of pasted or imported HTML; markdown round-trips.
- CRDT / sync (e.g. Yrs) in WASM next to a JS editor host ( Collaboration & CRDT).
- Optional layout when you don’t rely on the browser for line breaks in the body (advanced).
Choosing & next steps
Default path: contenteditable surface + WASM core, explicit source-of-truth rules, and a reconciliation loop you can trace in the debugger. Measure boundary traffic before optimizing rendering.
Read next: IME & composition · JS ↔ WASM boundary · Undo & redo model
Wasm guides
IME & composition
composition events, syncing a Rust document model, and why the browser still owns the IME.
JS ↔ WASM boundary
Strings, copies, batched ops, async vs input events, and keeping the hot path cheap.
Clipboard & input routing
beforeinput, paste, routing decisions in JS vs sanitization in WASM.
Tooling, bundle & workers
wasm-pack, wasm-opt, code splitting, Web Workers, COOP/COEP and threads.
Collaboration & CRDT (WASM)
Yrs/y-crdt, bridging to Yjs, snapshots vs update streams with an editor host.
Selection, Range & offsets
UTF-16 vs UTF-8 indices, Selection/Range in JS, mapping to a Rust model and getTargetRanges.
Undo & redo model
Browser undo stack vs model history, programmatic DOM, and WASM-hosted transactions.
Accessibility (WASM host)
Roles, focus, screen readers when the editable surface is still the browser.
Testing & debugging
E2E, profiling the JS↔WASM boundary, reproducing IME and paste in CI.
Security & deployment
CSP, SRI, module integrity, and hosting WASM next to contenteditable.