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