For more tips like this, sign up to the weekly newsletter!

Implement Drag & Drop with RxJs

Coordinating multiple event sources through time is a complex task. It usually involves keeping state variables around, heavily influencing how hard it is to understand the code.

A textbook example is a drag-and-drop functionality. At the very least, it requires the following functionality:

  • Record where the user clicked when initiated the drag
  • Do some modifications on drag, like adding a class
  • Handle mouse move events and update the element's position
  • Handle mouse up events and also do some modifications, like removing a class

Fortunately, with RxJs this can be handled in an elegant way without introducing any variables. Let's compose an observable that emits {top, left} values on drag!

First, we need the events:

const box = document.querySelector("#box");

const mouseDowns = rxjs.fromEvent(box, "mousedown");
const mouseUps = rxjs.fromEvent(window, "mouseup");
const mouseMoves = rxjs.fromEvent(window, "mousemove");

These are all observables, and the DOM handlers are only added when they are needed.

Let's move on to the mouse down events!

Since we want multiple values for a single mouse down event, a flatMap is a good candidate to use here. Add the class and save the box positions:

mouseDowns.pipe(
  rxjs.operators.flatMap((mouseDown) => {
    box.classList.add("dragging");
    
    const {left: startLeft, top: startTop} = box.getBoundingClientRect();
    
    return mouseMoves;
    // do something with the mouseMoves
  })
)

Then, we need to calculate the position of the drag for every mouse move:

return mouseMoves
  .pipe(
    rxjs.operators.map((mouseMove) => {
      return {
        top: mouseMove.clientY - (mouseDown.clientY - startTop),
        left: mouseMove.clientX - (mouseDown.clientX - startLeft),
      }
    }),
    ...
  )

And finally, terminate the mouse move events when a mouse up occurs, and also remove the class:

rxjs.operators.takeUntil(
  mouseUps.pipe(
    rxjs.operators.tap(() => box.classList.remove("dragging"))
  )
)

The last thing is to subscribe to the observable and change the position of the box:

.subscribe(({top, left}) => {
    box.style.top = top;
    box.style.left = left;
})
Try it
References
Learn more: