Status: documentation-only — no new code. Describes the implicit contract between
gesture,dndand the rest of the rendering pipeline.
TL;DR
The gesture (pinch / pan / rotate / swipe / long-press) and dnd
(HTML5 drag-and-drop + DataTransfer) modules do not use the classic reactive
pipeline (signal, ui.bind, ui.list). They operate as a direct
side-channel on DOM nodes, for two reasons:
- Latency — a 120 Hz gesture cannot afford a diffing loop. The
pointermovehandler writestransform: translate3d(…)directly on the targeted element, bypassing template/render. - Native APIs —
DragEvent.dataTransfer,pointercancel, pointer capture (setPointerCapture) are designed to operate on the originating element. Indirection = loss of browser context.
Consequences for the developer
1. State managed by the module, not by signal
// ✗ Anti-pattern: trying to bind a drag position to a signal
const x = signal.create(0);
ui.bind('#card', el => el.style.left = x.get() + 'px');
const g = gesture.attach(card, { onPan: ({ dx }) => x.set(x.get() + dx) });
// → 2× re-render per frame, guaranteed flickering
// ✓ Good pattern: let gesture write directly, read final state on drop
const g = gesture.attach(card, {
onPanEnd: ({ dx, dy }) => {
// Persist the final state HERE (a single signal.set per drop)
position.set({ x: position.get().x + dx, y: position.get().y + dy });
}
});
2. Re-render = active gesture lost
If during a gesture you replace the node (ui.list in replace mode,
template.create that re-injects a subtree), the gesture is cancelled by
the browser — pointercancel is emitted. Mitigations:
- Prefer
onUpdate: 'patch'or'none'on lists while a gesture is active. - Mark the gesture zone with
data-fw-no-rerender(internal convention) and skip it in reconciliation.
3. dnd is asymmetric
The HTML5 DnD API requires:
- the source carries
draggable="true"and adragstartlistener that fillsdataTransfer. - the target carries a
dragoverlistener that must calle.preventDefault()otherwisedropdoes not fire.
The framework's dnd module hides this mechanism behind
dnd.draggable(el, opts) / dnd.dropTarget(el, opts), but the data flow
remains outside signal. Transfer happens via dataTransfer.setData(type, json)
and reading on drop. Do not try to synchronise source ↔ target via a shared
signal during the drag: the browser already has that channel.
4. Touch vs mouse vs pen
All module gestures use pointerdown / pointermove / pointerup,
which unify the three. Do not listen to mousedown / touchstart in
parallel — you would get duplicates on hybrid devices (touch laptops).
The module already handles this case.
5. Cleanup
gesture.attach(el, opts) returns a handle { on, off, detach, dispose } —
dispose() (alias detach()) detaches all listeners and releases pointer
capture. Always call it in ui.onUnmount, otherwise listeners
survive node replacement:
const handle = gesture.attach(el, { onPan });
ui.onUnmount('card', () => handle.dispose());
ui.adopt('card', () => handle.dispose()) is strictly equivalent.
Why not a unified channel?
Recurring question: "why not also pass gesture / dnd through
signal + ui.bind to have a homogeneous API?"
Evaluated trade-offs:
| Option | Latency | Code | Browser DnD compat |
|---|---|---|---|
| Direct side-channel (current) | ~1 ms | native | full |
signal pipeline + diff |
~16 ms | abstract | DnD broken (async preventDefault) |
Hybrid (signal for onEnd) |
~1 ms | mixed | full |
The framework adopts the 3rd option: side-channel for movement, signal for final state.
Anti-patterns to avoid
- Do not call
signal.setinonMove(re-render at 120 Hz). - Do not listen to
mousedown+touchstartalongside the gesture module. - Do not unmount the target node while a gesture is in progress
(
ui.clear(parent),list.syncinreplace). - Do not forget
preventDefault()ondragover(the module does it, but not if you bypass the API). - Do not store
dataTransferbeyond thedrophandler (object cleared by the browser at the end of the event).
See also
src/dom/query/gesture.js— gesture module (pan/pinch/rotate/swipe/long-press)src/dom/query/dnd.js— dnd module (source/target/file-drop)docs/guide/rendering-pipeline.md— whysignalcannot keep up with 120 Hzdocs/guide/security.md— cross-origin DnD (files, URL, JSON)