docs/solutions/developer-experience/2026-05-02-slate-dom-incomplete-work-should-start-with-internal-coverage-boundaries.md
Slate v2 needs first-class support for model content whose DOM is intentionally absent or temporarily unmounted. The risky path is shipping a public collapse API first and only later discovering that selection, copy/paste, IME, mobile, browser find, and stale DOM behavior need a lower-level runtime contract.
editor.dom.assertDOMPoint(...)
and throws because the Slate node has no mapped DOM node.slots.HiddenRange / slots.HiddenSelf was too early.
That commits to product-shaped "hidden" vocabulary before the bridge contract
is proven.Start with an internal DOM coverage primitive:
DOMCoverage.registerBoundary(editor, {
boundaryId: 'section-body',
coveredPathRanges: [{ anchor: [0, 1], focus: [0, 1] }],
reason: 'app-collapse',
selectionPolicy: 'boundary',
copyPolicy: 'include-model',
findPolicy: 'not-native-until-mounted',
state: 'intentionally-hidden',
// owner/runtime metadata omitted
})
Add boundary-aware lookup before changing public rendering APIs:
DOMCoverage.resolveDOMPointOrBoundary(editor, hiddenPoint)
The first tracer should prove:
editor.dom.assertDOMPoint(hiddenPoint) still fails for missing DOM;DOMCoverage.resolveDOMPointOrBoundary(...) returns the boundary instead of
calling normal DOM lookup;slots.Boundary, slots.SelfBoundary, HiddenRange, or
HiddenSelf ships before the proof matrix is green.Once the proof matrix is green, expose only the narrow unstable adapter:
renderElement={({ children, element, slots }) => {
if (element.type !== 'section') {
return <EditableElement>{children}</EditableElement>
}
const childNodes = React.Children.toArray(children)
return (
<EditableElement>
{childNodes[0]}
<slots.unstableBoundary
boundaryId="section-body"
mounted={false}
scope={{ from: 1, type: 'children' }}
>
Collapsed body
</slots.unstableBoundary>
</EditableElement>
)
}}
The unstable adapter should support child-range and self scopes without raw
runtime ids. Keep stable slots.Boundary for a later docs/adoption pass.
The private harness should store covered runtime endpoints, not only path ranges. That lets the registry distinguish safe structural movement from stale coverage:
For lookup scale, bucket registered boundaries by covered root key and refresh the index from the editor snapshot version. That keeps unrelated point lookup cheap in the 5000-block / 100-boundary stress case without committing to a full virtualization-grade interval tree yet.
For performance, add surface-weight profile counters to the existing benchmark instead of creating a new lane too early:
surface-weight:dom-node-countsurface-weight:dom-nodes-per-blocksurface-weight:editable-descendant-countsurface-weight:editable-descendants-per-blocksurface-weight:root-group-countsurface-weight:slate-element-countsurface-weight:slate-text-countsurface-weight:slate-leaf-countsurface-weight:shell-countThe core invariant is not "collapsed UI." It is model-present content with incomplete DOM coverage. That same primitive can later cover app collapse, large-document staged mounting, aggressive shell mode, atom boundaries, and future virtualization with different policies.
Keeping the first slice internal avoids fossilizing the wrong public API. The bridge can learn how to resolve model points/ranges, placeholders, copy/paste, and materialization without promising an app-facing React slot shape.
After the bridge proof is green, slots.unstableBoundary is acceptable because
it is just a React authoring adapter over the internal primitive. It still keeps
the important policy centralized: the app chooses mounted vs hidden and the
scope; Slate owns registration, placeholder import, model-backed copy, and dev
safety.
The profile counters matter because staging or shelling can make startup look good while the repeated editable unit stays bloated. Counting DOM and editable surface weight keeps the benchmark honest.
renderElement children mandatory until the boundary bridge is
proven.node --check on the wrapper script.