Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating an observable 'completed' event in RxJS

Given: the reactive extensions drag and drop example , how would you subscribe to just a drop event?

I have modified the code to subscribe to a 'completed' callback, but it does not complete.

    (function (global) {

    function main () {
        var dragTarget = document.getElementById('dragTarget');
        var $dragTarget = $(dragTarget);

        // Get the three major events
        var mouseup  = Rx.Observable.fromEvent(document, 'mouseup');
        var mousemove = Rx.Observable.fromEvent(document,    'mousemove');
        var mousedown = Rx.Observable.fromEvent(dragTarget, 'mousedown');

        var mousedrag = mousedown
            .filter(function(md){                   
                //console.log(md.offsetX + ", " + md.offsetY);
                return  md.offsetX <= 100
                        ||
                        md.offsetY <= 100;
            })
            .flatMap(function (md) {

                // calculate offsets when mouse down
                var startX = md.offsetX, startY = md.offsetY;

                // Calculate delta with mousemove until mouseup
                return mousemove.map(function (mm) {
                    mm.preventDefault();

                    return {
                        left: mm.clientX - startX,
                        top: mm.clientY - startY
                    };
                }).takeUntil(mouseup);
            });


        // Update position
        var subscription = mousedrag.subscribe(
        function (pos) {                    
            dragTarget.style.top = pos.top + 'px';
            dragTarget.style.left = pos.left + 'px';
        },
        function(errorToIgnore) {},
        function() {    alert('drop');});

    }

    main();

}(window));

I have read that hot observables, such as those that have been created from mouse events, never 'complete'. Is this correct? How can I otherwise get a callback on 'drop'?

like image 962
bnieland Avatar asked Jun 13 '14 21:06

bnieland


People also ask

How do you make an observable complete?

I found an easier way to do this for my use case, If you want to do something when the observable is complete then you can use this: const subscription$ = interval(1000). pipe( finalize(() => console. log("Do Something")), ).

How do I create an observable in RXJS?

Creating Observableslink import { Observable } from 'rxjs'; const observable = new Observable(function subscribe(subscriber) { const id = setInterval(() => { subscriber. next('hi') }, 1000); });

Which of the following will create an observable from an event using RXJS in Angular 6?

Angular provides FromEvent method to create an observable from DOM events directly.

Do I need to unsubscribe from a completed observable RXJS?

complete() after it has emitted all of it's values. There's no need to unsubscribe. It completes on it's own, which means it unsubscribes all subscribers automatically. This is also the reason why you don't often notice any memory leaks.


2 Answers

Something like this should do the trick.

(function (global) {

    function main () {
        var dragTarget = document.getElementById('dragTarget');

        // Get the three major events
        var mouseup = Rx.Observable.fromEvent(document, 'mouseup');
        var mousemove = Rx.Observable.fromEvent(document, 'mousemove');
        var mousedown = Rx.Observable.fromEvent(dragTarget, 'mousedown');

        var drop = mousedown
                .selectMany(
                    Rx.Observable
                        .concat(
                            [
                                mousemove.take(1).ignoreElements(),
                                mouseup.take(1)
                            ]
                        )
                );
    }

    main();

}(window));

Edit:

If you think of an observable as an asynchronous function which yields multiple values, and then possibly completes or errors, you'll immediately recognize that there can only be one completion event.

When you start composing multiple function, the outer-most function still only completes once, even if that function contains multiple functions inside of it. So even though the total number of "completions" is 3, the outer-most function still only completes once.

Basically, that means if the outer-most function is suppose to return a value each time a drag completes, you need a way of actually doing that. You need to translate the drag completion into an "onNext" event for the outer-most observable.

ANY which way you can do that is going to get you what you want. Maybe that's the only kind of events that the outer-most function returns, or maybe it also returns drag starts and moves, but so long as it returns the drag completions, you'll end up with what you need (even if you have to filter it later).

The example I've given above is just one way to return the drag drops in the outer-most observable.

like image 81
cwharris Avatar answered Oct 19 '22 16:10

cwharris


OP's link to the official example was down, it's here:

https://github.com/Reactive-Extensions/RxJS/blob/master/examples/dragndrop/dragndrop.js

There are two solutions to the original question, using either the primitive mouse events expanding upon the official example, or the native HTML5 drag and drop events.

Composing from 'mouseup', 'mousedown' and 'mousemove'

First we use mousedown1.switchMapTo(mousemove.takeUntil(mouseup).take(1)) to get the 'drag start' stream. switchMapTo (see doc) uses a combination of flattening (mapping 'mousedown1' to each of the first emit of 'mouse moving until mouse up', aka 'dragging') and switch (see doc), giving us the latest mousedown1 emit followed by dragging, i.e. not just any time you click your mouse on the box.

Another 'mousedown' stream mousedown2 is used to compose a mousedrag stream so we can constantly render the box while dragging. mousedown1 above is prioritised over mousedown2. See https://stackoverflow.com/a/35964479/232288 for more information.

Once we have our 'drag start' stream we can get the 'drag stop' stream via: mousedragstart.mergeMapTo(mouseup.take(1)). mergeMapTo (see doc) maps 'mousedragstart' to each of the first emit of 'mouseup', giving us the latest 'mousedragstart' followed by 'mouseup', which is essentially 'drag stop'.

The demo below works with RxJS v5:

const { fromEvent } = Rx.Observable;
const target = document.querySelector('.box');
const events = document.querySelector('#events');

const mouseup = fromEvent(target, 'mouseup');
const mousemove = fromEvent(document, 'mousemove');
const [mousedown1, mousedown2] = prioritisingClone(fromEvent(target, 'mousedown'));

const mousedrag = mousedown2.mergeMap((e) => {

  const startX = e.clientX + window.scrollX;
  const startY = e.clientY + window.scrollY;
  const startLeft = parseInt(e.target.style.left, 10) || 0;
  const startTop = parseInt(e.target.style.top, 10) || 0;

  return mousemove.map((e2) => {
    e2.preventDefault();

    return {
      left: startLeft + e2.clientX - startX,
      top: startTop + e2.clientY - startY
    };
  }).takeUntil(mouseup);
});

// map the latest mouse down emit to the first mouse drag emit, i.e. emits after pressing down and
// then dragging.
const mousedragstart = mousedown1.switchMapTo(mousemove.takeUntil(mouseup).take(1));

// map the mouse drag start stream to first emit of a mouse up stream, i.e. emits after dragging and
// then releasing mouse button.
const mousedragstop = mousedragstart.mergeMapTo(mouseup.take(1));

mousedrag.subscribe((pos) => {
  target.style.top = pos.top + 'px';
  target.style.left = pos.left + 'px';
});

mousedragstart.subscribe(() => {
  console.log('Dragging started');
  events.innerText = 'Dragging started';
});

mousedragstop.subscribe(() => {
  console.log('Dragging stopped');
  events.innerText = 'Dragging stopped';
});

function prioritisingClone(stream$) {
  const first = new Rx.Subject();
  const second = stream$.do(x => first.next(x)).share();

  return [
    Rx.Observable.using(
      () => second.subscribe(() => {}),
      () => first
    ),
    second,
  ];
}
.box {
  position: relative;
  width: 150px;
  height: 150px;
  background: seagreen;
  cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.0.3/Rx.js"></script>
<div class="box"></div>

<h3 id="events"></h3>

Composing from HTML5 native 'dragstart', 'dragover' and 'drop'

The native events make things a little easier. Just make sure the dragover observer calls e.preventDefault() so the body element can become a valid drop zone. See more info.

We use switchMap (see doc) in a similar fashion as how switchMapTo is used above: flatten and map the latest 'dragstart' to the latest 'drop' to get our 'drag then drop' stream.

The difference is only when the user drops the div do we update the position.

const { fromEvent } = Rx.Observable;
const target = document.querySelector('.box');
const events = document.querySelector('#events');

const dragstart = fromEvent(target, 'dragstart');
const dragover = fromEvent(document.body, 'dragover');
const drop = fromEvent(document.body, 'drop');

const dragthendrop = dragstart.switchMap((e) => {
  const startX = e.clientX + window.scrollX;
  const startY = e.clientY + window.scrollY;
  const startLeft = parseInt(e.target.style.left, 10) || 0;
  const startTop = parseInt(e.target.style.top, 10) || 0;
  // set dataTransfer for Firefox
  e.dataTransfer.setData('text/html', null);
  console.log('Dragging started');
  events.innerText = 'Dragging started';

  return drop
    .take(1)
    .map((e2) => {
      return {
        left: startLeft + e2.clientX - startX,
        top: startTop + e2.clientY - startY
      };
    });
});

dragover.subscribe((e) => {
  // make it accepting drop events
  e.preventDefault();
});

dragthendrop.subscribe((pos) => {
  target.style.top = `${pos.top}px`;
  target.style.left = `${pos.left}px`;
  console.log('Dragging stopped');
  events.innerText = 'Dragging stopped';
});
.box {
  position: relative;
  width: 150px;
  height: 150px;
  background: seagreen;
  cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.0.3/Rx.js"></script>
<div class="box" draggable="true"></div>

<h3 id="events"></h3>
like image 23
Dan7 Avatar answered Oct 19 '22 17:10

Dan7