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
Card A drag me
Card B drag me
Card C drag me
active card:
x:
y:
dropped in zone: 0
method:

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';
    }
  });
}

see also

references