How to implement drag & drop using RxJS
Tags: javascript, rxjs, functional, webdev
Published: 2021-07-16 18:57:22

Drag & drop is one of the features that can be very useful for the end-users of our application. Additionally, it is a great example to show how RxJS can be used to handle drag-and-drop functionality with ease. Let's see how we can implement the simple dragging behavior.

To follow along with all the code examples in this article, I recommend opening [this Stackblitz starter example](https://stackblitz.com/edit/rxjs-drag-and-drop-starter). All examples will be based on this starter project.

## Define drag and drop

Before we start the implementation, let's consider what the drag and drop functionality consists of. It can be split into 3 phases:
- drag start
- drag move
- drag end (drop)

In a nutshell, `drag start` happens **whenever** we press **mouse down** on a draggable item. Following that each time we **move a cursor** a `drag move` event should be emitted. Drag move **should continue**, but only **until** we **release** the mouse button (mouse up event).

## Basic implementation

You might have noticed that a few of the words above are bolded. This is because those specific words give us a clue on how we can implement the described behavior. For starters, we can see that 3 native events will be necessary to implement our feature:
- `mousedown` - for starting the dragging
- `mousemove` - for moving the dragged element
- `mouseup` - for ending the dragging (dropping an element)

Let's first create Observables out of those events. They will be our basic building blocks.

```js
import { fromEvent } from 'rxjs'

const draggableElement = document.getElementById('dragMe');

const mouseDown$ = fromEvent(draggableElement, 'mousedown');
const mouseMove$ = fromEvent(draggableElement, 'mousemove');
const mouseUp$ = fromEvent(draggableElement, 'mouseup');
```

We now have our base events. Now, let's create our drag event from them.

```js
import { switchMap, takeUntil } from 'rxjs/operators';

const dragStart$ = mouseDown$;
const dragMove$ = dragStart$.pipe( // whenever we press mouse down
    switchMap(() => mouseMove$).pipe( // each time we move a cursor
      takeUntil(mouseUp$) // but only until we release the mouse button
    ),
);
```

As you can see, due to the very declarative syntax of RxJS, we were able to transform the previous [definition](#define-drag-and-drop).

This is a good start, but we need a bit more information in the `dragMove$` Observable so that we know how far we drag the element. For that, we can use the value emitted by `dragStart$`, and compare it with each value emitted by `mouseMove$`:

```js
const dragMove$ = dragStart$.pipe(
  switchMap(start =>
    mouseMove$.pipe(
      // we transform the mouseDown and mouseMove event to get the necessary information
      map(moveEvent => ({
        originalEvent: moveEvent,
        deltaX: moveEvent.pageX - start.pageX,
        deltaY: moveEvent.pageY - start.pageY,
        startOffsetX: start.offsetX,
        startOffsetY: start.offsetY
      })),
      takeUntil(mouseUp$)
    )
  ),
);
```

Now, our Observable emits all necessary information for us to move the dragged element with the mouse moving. Since observables are lazy, we need to `subscribe` it to perform any action.

```js
dragMove$.subscribe(move => {
  const offsetX = move.originalEvent.x - move.startOffsetX;
  const offsetY = move.originalEvent.y - move.startOffsetY;
  draggableElement.style.left = offsetX + 'px';
  draggableElement.style.top = offsetY + 'px';
});
```

This works well, but only if we don't move the mouse too fast. This is because our `mouseMove$` and `mouseUp$` events are listening on the dragged element itself. If the mouse moves too fast, the cursor can leave the dragged element, and then we will stop receiving the `mousemove` event. The easy solution to this is to target `mouseMove$` and `mouseUp$` to the `document` so that we receive all the mouse events even if we leave the dragged element for a moment.

```js
const mouseMove$ = fromEvent(document, 'mousemove');
const mouseUp$ = fromEvent(document, 'mouseup');
```

This small change will improve the dragging behavior so that we can move the cursor freely around the whole document.

Before we continue, let's clean the code by extracting the logic we've created into a function.

```js
const mouseMove$ = fromEvent(document, 'mousemove');
const mouseUp$ = fromEvent(document, 'mouseup');

const draggableElement = document.getElementById('dragMe');

createDraggableElement(draggableElement);

function createDraggableElement(element) {
  const mouseDown$ = fromEvent(element, 'mousedown');

  const dragStart$ = mouseDown$;
  const dragMove$ = dragStart$.pipe(
    switchMap(start =>
      mouseMove$.pipe(
        map(moveEvent => ({
          originalEvent: moveEvent,
          deltaX: moveEvent.pageX - start.pageX,
          deltaY: moveEvent.pageY - start.pageY,
          startOffsetX: start.offsetX,
          startOffsetY: start.offsetY
        })),
        takeUntil(mouseUp$)
      )
    )
  );

  dragMove$.subscribe(move => {
    const offsetX = move.originalEvent.x - move.startOffsetX;
    const offsetY = move.originalEvent.y - move.startOffsetY;
    element.style.left = offsetX + 'px';
    element.style.top = offsetY + 'px';
  });
}
```

This way, we can easily make our code so that it allows for multiple draggable elements:

```js
appDiv.innerHTML = `
  <h1>RxJS Drag and Drop</h1>
  <div class="draggable"></div>
  <div class="draggable"></div>
  <div class="draggable"></div>
`;

const draggableElements = document.getElementsByClassName('draggable');

Array.from(draggableElements).forEach(createDraggableElement);
```

In case you have any trouble during any of the steps, you can compare your solution with [this example](https://stackblitz.com/edit/rxjs-drag-and-drop-function).

## Emitting custom events

The above example shows that it is possible to implement a simple dragging behavior using RxJS. In real-life examples, it might be very useful to have a custom event on a draggable element so that it is easy to register your custom function to any part of the drag & drop lifecycle.

It the previous example, we defined `dragStart$` and `dragMove$` observables. We can use those directly to start emitting `mydragstart` and `mydragmove` events on the element accordingly. I've added a `my` prefix to make sure I don't collide with any native event.

```js
  import { tap } from 'rxjs/operators';

   dragStart$
    .pipe(
      tap(event => {
        element.dispatchEvent(
          new CustomEvent('mydragstart', { detail: event })
        );
      })
    )
    .subscribe();

  dragMove$
    .pipe(
      tap(event => {
        element.dispatchEvent(
          new CustomEvent('mydragmove', { detail: event })
        );
      })
    )
    .subscribe();
```

As you might see in the example above, I'm putting dispatching logic into a `tap` function. This is an approach I recommend as this allows us to combine multiple observable streams into one and call `subscribe` only once:

```js
import { combineLatest } from 'rxjs';

combineLatest([
    dragStart$.pipe(
      tap(event => {
        element.dispatchEvent(
          new CustomEvent('mydragstart', { detail: event })
        );
      })
    ),
    dragMove$.pipe(
      tap(event => {
        element.dispatchEvent(
          new CustomEvent('mydragmove', { detail: event })
        );
      })
    )
  ]).subscribe();
```

Now the only event missing is `mydragend`. This event should be emitted as the last event of the `mydragmove` event sequence. We can again use the RxJS operator to achieve such behavior.

```js
  const dragEnd$ = dragStart$.pipe(
    switchMap(start =>
      mouseMove$.pipe(
        map(moveEvent => ({
          originalEvent: moveEvent,
          deltaX: moveEvent.pageX - start.pageX,
          deltaY: moveEvent.pageY - start.pageY,
          startOffsetX: start.offsetX,
          startOffsetY: start.offsetY
        })),
        takeUntil(mouseUp$),
        last(),
      )
    )
  );
```

And the last step would be to emit this event alongside the others

```js
combineLatest([
    dragStart$.pipe(
      tap(event => {
        element.dispatchEvent(
          new CustomEvent('mydragstart', { detail: event })
        );
      })
    ),
    dragMove$.pipe(
      tap(event => {
        element.dispatchEvent(new CustomEvent('mydragmove', { detail: event }));
      })
    ),
    dragEnd$.pipe(
      tap(event => {
        element.dispatchEvent(new CustomEvent('mydragend', { detail: event }));
      })
    )
  ]).subscribe();
```

This concludes the implementation. We can now use those events any way we want to.

```js
Array.from(draggableElements).forEach((element, i) => {
  element.addEventListener('mydragstart', () =>
    console.log(`mydragstart on element #${i}`)
  );

  element.addEventListener('mydragmove', event =>
    console.log(
      `mydragmove on element #${i}`,
      `delta: (${event.detail.deltaX}, ${event.detail.deltaY})`
    )
  );

  element.addEventListener('mydragend', event =>
    console.log(
      `mydragend on element #${i}`,
      `delta: (${event.detail.deltaX}, ${event.detail.deltaY})`
    )
  );
});
```

You can find the whole implementation [here](https://stackblitz.com/edit/rxjs-drag-and-drop-events), or you can play with it below:

<iframe src="https://stackblitz.com/edit/rxjs-drag-and-drop-events?embed=1&file=index.js" width="100%" height="500px" title="RxJS Drag and Drop"></iframe>

## Conclusion

In this article, I've shown you that you can easily implement a basic drag-and-drop behavior by using RxJS. It is a great tool for this use case as it makes managing the stream of events easier, and allows for the very declarative implementation of complex behaviors.

If you are looking for more interesting examples of how you can use the drag-and-drop events with RxJS, I recommend visiting [this example](https://stackblitz.com/edit/rxjs-magic-hat).

In case you have any questions, you can always tweet or DM me at [@ktrz](https://twitter.com/ktrz__). I'm always happy to help!

---

_This Dot Labs is a modern web consultancy focused on helping companies realize their digital transformation efforts. For expert architectural guidance, training, or consulting in React, Angular, Vue, Web Components, GraphQL, Node, Bazel, or Polymer, visit [thisdotlabs.com](https://www.thisdotlabs.com)._

_This Dot Media is focused on creating an inclusive and educational web for all. We keep you up to date with advancements in the modern web through events, podcasts, and free content. To learn, visit [thisdot.co](https://www.thisdot.co)._