v135 · javascript
Drag as Observable
Mouse drag is three events — pointerdown, pointermove, pointerup — that form a natural Observable: the down starts a stream, moves are the values, up terminates it. takeUntil expresses this in one declarative chain. No cleanup flags, no stale listeners.
Feature detection: checking…
Before: imperative event bookkeeping
let dragging = false;
let startX, startY, origX, origY;
el.addEventListener('pointerdown', e => {
dragging = true;
startX = e.clientX; startY = e.clientY;
origX = el.offsetLeft; origY = el.offsetTop;
document.addEventListener('pointermove', onMove);
document.addEventListener('pointerup', onUp);
});
function onMove(e) {
if (!dragging) return; // guard needed
el.style.left = origX + (e.clientX - startX) + 'px';
el.style.top = origY + (e.clientY - startY) + 'px';
}
function onUp() {
dragging = false; // must remember to reset
document.removeEventListener('pointermove', onMove);
document.removeEventListener('pointerup', onUp);
}
After: Observable flatMap + takeUntil
el.when('pointerdown').flatMap(down => {
const startX = down.clientX;
const startY = down.clientY;
const origX = el.offsetLeft;
const origY = el.offsetTop;
return document.when('pointermove')
.takeUntil(document.when('pointerup'))
.map(move => ({
x: origX + (move.clientX - startX),
y: origY + (move.clientY - startY),
}));
}).subscribe({
next({ x, y }) {
el.style.left = x + 'px';
el.style.top = y + 'px';
},
});
Drag the cards — try dropping into the zone →
drop
zone
zone
Card A
drag me
Card B
drag me
Card C
drag me
Drag event log:
Start dragging to see Observable events.
// Observable drag — no cleanup state needed
function makeDraggable(el, stage) {
el.when('pointerdown').flatMap(down => {
const startX = down.clientX - el.offsetLeft;
const startY = down.clientY - el.offsetTop;
el.setPointerCapture(down.pointerId);
el.classList.add('dragging');
return stage.when('pointermove')
.takeUntil(stage.when('pointerup').take(1))
.map(e => ({ x: e.clientX - startX, y: e.clientY - startY }))
.do({ complete() { el.classList.remove('dragging'); } });
}).subscribe({
next({ x, y }) {
el.style.left = clamp(x, 0, maxX) + 'px';
el.style.top = clamp(y, 0, maxY) + 'px';
}
});
}