RXJS drag and drop onDrop/onDragOver - rxjs

I was trying to do the drag and drop by using RXJS.
Does anybody know how to do the onDrop or onDragOver properly with the RXJS?
Here is what I have tried. But not working...
let mouseup = Observable.fromEvent(document, 'mouseup');
let mousemove = Observable.fromEvent(document, 'mousemove');
let mousedown = Observable.fromEvent(target, 'mousedown');
const drop = mousedown
.flatMap((md) => {
return Observable
.concat(
[
mousemove.take(1).ignoreElements(),
mouseup.take(1)
]
);
});
Here is another try, which seems to be working. But it will fire whenever mouseup happens, not only fire after the drag.
const drop = mousemove
.switchMap((mm) => mouseup.take(1));

If you want to have all your drag events, you can use this here
let up$ = Observable.fromEvent(document, 'mouseup');
let move$ = Observable.fromEvent(document, 'mousemove');
let down$ = Observable.fromEvent(document, 'mousedown');
let drag$ = down$.switchMap(down => move$.takeUntil(up$));
This will emit every mousemove event which was triggered while you 'dragged'.
It's also very easy to adapt to your needs. Let's say you need the x-distance of your drag:
let distance$ = down$.switchMap((down: MouseEvent) =>
move$.map((move: MouseEvent) => (move.clientX - down.clientX))
.takeUntil(up$));
You want the same, but also support touch? No problem! Just merge your three input streams with their touch-equivalent and you are good to go.
I hope this answers your question,
Ben

Related

How to make this RxJs code more elegant? The code recording mouse hover time on a specific area

I want to record mouse hover time on a specific area such as a 'div' container box area , by using RxJs.
const element = document.querySelector('#some-div');
let totalHoverTime = 0;
const INTERVAL = 100;
const mousePos = {
x: -1,
y: -1
};
const accumulate = () => {
const x = mousePos.x;
const y = mousePos.y;
const divx1 = element.offsetLeft;
const divy1 = element.offsetTop;
const divx2 = element.offsetLeft + element.offsetWidth;
const divy2 = element.offsetTop + element.offsetHeight;
// out of area
if (x < divx1 || x > divx2 || y < divy1 || y > divy2) {
console.log('out')
} else {
// in area
console.log('in')
totalHoverTime += INTERVAL;
}
};
const accumulateTimer = rx.interval(INTERVAL);
accumulateTimer.subscribe(() => {
accumulate();
});
rx
.fromEvent(element, 'mousemove')
.pipe(rxOp.debounce(() => rx.timer(INTERVAL)))
.subscribe((e: MouseEvent) => {
mousePos.x = e.clientX;
mousePos.y = e.clientY;
});
I'm not very familiar with rxjs, I think this code may can be more elegant to implement.
Optimized code
Thank you very much for your answers. #hugo #der_berni
const element = document.body;
const INTERVAL = 2000;
const withinBounds = ({ x, y }: { x: number; y: number }) => {
const divx1 = element.offsetLeft;
const divy1 = element.offsetTop;
const divx2 = element.offsetLeft + element.offsetWidth;
const divy2 = element.offsetTop + element.offsetHeight;
const outOfBounds = x < divx1 || x > divx2 || y < divy1 || y > divy2;
if (outOfBounds) {
// out of area
console.log('out');
} else {
// in area
console.log('in');
}
return !outOfBounds;
};
const mousePositions = rx
.fromEvent(document, 'mousemove')
.pipe(rxOp.throttleTime(200))
.pipe(rxOp.map((e: MouseEvent) => ({ x: e.pageX, y: e.pageY })));
const mousePositionIsValid = mousePositions
.pipe(rxOp.map(withinBounds))
.pipe(rxOp.distinctUntilChanged());
const hoverTimer = mousePositionIsValid.pipe(rxOp.switchMap(valid => (valid ? accumulateTimer : rx.empty())));
const totalHoverTime = hoverTimer.pipe(rxOp.scan((x, y) => x + INTERVAL, -500)); // The first time mouse moves in, this will be triggered once, so it is set to -500, and the first time it comes in is 0ms.
totalHoverTime.subscribe(hoverTime => {
console.log('totalHoverTime is:', hoverTime);
});
Finally, I found that I still need to use mousemove event combined timer to implement this function. When the mouse is already hovering above the div on page load, the mouseenter event will never triggerd in my page seemly. Maybe only in jsfiddle can be no problem.
I' also only started using RxJS recently, so there might be a better way to solve your problem.
However, a huge improvement over your approach would already be to chain the observables and use the switchMap operator. One thing to keep in mind when working with rxjs is, that you want to avoid manual subscriptions, because you will have to keep track of them and unsubscribe yourself to prevent leaks. When using operators like switchMap, these keep track of the subscriptions to inner observables, and also automatically unsubscribe.
Following code snippet should solve your problem:
Rx.Observable.fromEvent(element, 'mouseenter') // returns Observable<Event>
.map(() => Date.now()) // transform to Observable<number>
.switchMap((startTime) => { // switches to new inner observable
return Rx.Observable.fromEvent(button, 'mouseleave')
// When the observable from mouseleave emmits, calculate the hover time
.map(() => Date.now() - startTime);
})
.subscribe((hoverTime) => {console.log(hoverTime)});
If you want to try it out, see this jsFiddle: https://jsfiddle.net/derberni/hLgw1yvj/3/
EDIT:
Even if your div is very large, and the mouse might never leave it and trigger the mouseleave event, this can be solved with rxjs. You just have to change when the observable emits, and for how long you let it emit before you complete it. The WHEN can be adapted, so that it emits in a set interval, and the UNTIL can be set with the rxjs function takeUntil. takeUntil receives an observable as an argument, and takes values from the source observable, until the 'argument' observable emits.
Check out this code and fiddle, which updates the hover time in 1s steps and when the mouseleave event triggers: https://jsfiddle.net/derberni/3cky0g4e/
let div = document.querySelector('.hover-target');
let text = document.querySelector('.hover-time');
Rx.Observable.fromEvent(div, 'mouseenter')
.map(() => Date.now())
.switchMap((startTime) => {
return Rx.Observable.merge(
Rx.Observable.interval(1000),
Rx.Observable.fromEvent(div, 'mouseleave')
)
.takeUntil(Rx.Observable.fromEvent(div, 'mouseleave'))
.map(() => Date.now() - startTime);
})
//.takeUntil(Rx.Observable.fromEvent(div, 'mouseleave'))
.subscribe((hoverTime) => {
text.innerHTML = "Hover time: " + hoverTime + "ms"
});
At least in the fiddle this works also when the mouse is already hovering above the div on page load, because then the mouseenter event is also triggered.
Point-free
The simplest thing: replace (x => f(x)) with simply f. It's equivalent and will read better in most cases. This:
accumulateTimer.subscribe(() => {
accumulate();
});
Becomes:
accumulateTimer.subscribe(accumulate);
Fat functions:
The accumulate function could be broken down into:
const accumulate = () => {
const x = mousePos.x;
const y = mousePos.y;
if (withinBounds(x, y)) {
totalHoverTime += INTERVAL;
}
};
const withinBounds = ({x, y}) => {
const divx1 = element.offsetLeft;
const divy1 = element.offsetTop;
const divx2 = element.offsetLeft + element.offsetWidth;
const divy2 = element.offsetTop + element.offsetHeight;
const outOfBounds = x < divx1 || x > divx2 || y < divy1 || y > divy2;
if (outOfBounds) {
// out of area
console.log('out')
} else {
// in area
console.log('in')
}
return !outOfBounds;
};
See how we separated withinBounds which is pretty big but performs a simple definite task, purely functionally (no side-effect, one input gives the same output) -- ignoring the debug calls that is. Now we don't have to think so hard about it and we can focus on accumulate.
Avoid side-effects & compose
The most glaring issue is the whole loop relying on a side effect on mousePos:
const mousePositions = rx
.fromEvent(element, 'mousemove')
.pipe(rxOp.debounce(() => rx.timer(INTERVAL)))
//.subscribe((e: MouseEvent) => {
// mousePos.x = e.clientX;
// mousePos.y = e.clientY;
//});
.map((e: MouseEvent) => ({ x: e.clientX, y: e.clientY )));
Don't subscribe and save the value, it breaks the idea of flow behind rxjs. Use the return value, Luke. More specifically, pipe further to refine it until you reach the desired data. Above, we have a stream that emits the mouse positions alone.
// Will emit true when the mouse enters and false when it leaves:
const mousePositionIsValid = mousePositions
.map(withinBounds)
.distinctUntilChanged();
// Fires every INTERVAL, only when mouse is within bounds:
const hoverTimer = mousePositionIsValid
.switchMap(valid => valid ? accumulateTimer : rx.empty())
(edited with switchMap as suggested by #der_berni)
You wrote a function named "accumulate". Whenever you say "accumulate", reduce (and the likes) should come to mind. Reduce emits a single aggregate value when the stream completes. Here we use scan to get a new updated value each time the underlying stream emits:
// For each element produced by hoverTimer, add INTERVAL
const totalHoverTime = hoverTimer.scan((x, y) => x + INTERVAL, 0);
Note that it doesn't add to global each time, but every value it emits is the previous one + INTERVAL. So you can subscribe to that to get your total hover time.

takeUntil failing to prevent emissions from observable

I am trying to create my own click, hold and drag events using Rxjs and the mousedown, mouseup and mousemove events. My attempts use a number of streams that begin with a mousedown event, each with a takeUntil that listens for emissions from the other streams. Basically once one of the streams has "claimed" the action (i.e. passed all the requirements and emitted a value) the other observables should complete with no emissions.
I have looked at other answers and thought it might have something to do with the timer running async but it happens between streams that do not rely the timer e.g. drag and click. I have been playing around in codesandbox.io using rxjs v6.
The takeUntil's also have to sit on the inner observables as I don't want the outer observables to run once and complete.
The code is shown below:
const mouse_Down$ = fromEvent(document, "mousedown").pipe(
tap(event => event.preventDefault())
);
const mouse_Up$ = fromEvent(document, "mouseup").pipe(
tap(event => event.preventDefault())
);
const mouse_Move$ = fromEvent(document, "mousemove");
const mouse_drag$ = mouse_Down$
.pipe(
mergeMap(mouseDownEvent =>
mouse_Move$.pipe(takeUntil(merge(mouse_Up$, mouse_Hold$, mouse_drag$)))
)
).subscribe(event => console.log("Drag"));
const mouse_Hold$ = mouse_Down$
.pipe(
mergeMap(mouseDownEvent =>
timer(1000).pipe(takeUntil(merge(mouse_drag$, mouse_Click$)))
)
).subscribe(event => console.log("Hold"));
const mouse_Click$ = mouse_Down$
.pipe(
mergeMap(mouseDownEvent =>
mouse_Up$.pipe(takeUntil(mouse_drag$, mouse_Hold$))
)
).subscribe(event => console.log("Click"));
Expected behaviour:
If the user moves the mouse within 1s of the mousedown event the mouse_drag$ stream should begin emitting and the mouse_Click$/mouse_Hold$'s inner observables should complete (thanks to the takeUntil(mouse_drag$) without emitting and await the next mouse_down$ emmission.
If the mouse button remains down for more than 1s without moving the mouse_Hold$ should emit and mouse_drag$/mouse_click$'s inner observable should complete (thanks to the takeUntil(mouse_Hold$) without emitting and await the next mouse_down$ emmission.
Actual Behaviour: Currently the mouse_Drag$ will emit, the mouse_Hold$ will emit after one second and the mouse_Click$ will emit when the button is released.
My question is why doesn't the emitting mouse_Drag$ stream cause the mouse_Hold$ and mouse_Click$'s inner observable to complete without emitting?
The take until needs to be at the end of your chain
This will cancel the whole chain.
const { fromEvent } = rxjs;
const { tap, takeUntil, mergeMap, merge } = rxjs.operators;
const mouse_Down$ = fromEvent(document, "mousedown").pipe(
tap(event => event.preventDefault())
);
const mouse_Up$ = fromEvent(document, "mouseup").pipe(
tap(event => event.preventDefault())
);
const mouse_Move$ = fromEvent(document, "mousemove");
const mouse_drag$ = mouse_Down$
.pipe(
mergeMap(mouseDownEvent =>
mouse_Move$
),
takeUntil(mouse_Up$)
).subscribe(event => console.log("Drag"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.4.0/rxjs.umd.min.js"></script>
To clarify:
that you want emit from mouse_Hold$ if mouse is hold more then 1 second.
You want to get values from mouse_drag$ if LESS then 1 second pass after mouse dropdown and mouseMove.
You do not have to complete anything since otherwise all behaviour will work only once.
So plan:
3. mouse_drag$ - If mousedown - check mouseMove for 1 second. If mouseMove emits - switch to mouseMove values
4. mouse_Hold$ - if mouseDown - check mouseMove for 1 second. If mouseMove doesn't emit - switch to mouseHold and make it emit 'Hold'
let Rx = window['rxjs'];
const {defer, of, timer, fromEvent, merge, race} = Rx;
const {switchMap, repeat, tap, takeUntil, filter} = Rx.operators;
const {ajax} = Rx.ajax;
console.clear();
const mouse_Down$ = fromEvent(document, "mousedown");
const mouse_Up$ = fromEvent(document, "mouseup");
const mouse_Move$ = fromEvent(document, "mousemove");
const timer$ = timer(2000);
mouse_Hold$ = mouse_Down$.pipe(
switchMap((downEvent) => {
return timer$.pipe(
switchMap((time) => of('HOLD'))
);
}),
takeUntil(merge(mouse_Up$, mouse_Move$)),
repeat(mouse_Down$)
)
mouse_Hold$.subscribe(console.warn);
mouse_drags$ = mouse_Down$.pipe(
switchMap(() => mouse_Move$),
takeUntil(mouse_Up$, $mouse_Hold),
repeat(mouse_Down$)
)
mouse_drags$.subscribe(console.log);
Here is a codepen: https://codepen.io/kievsash/pen/oOmMwp?editors=0010

Toggle between data streams and make them pause/play with RxJs

I have getUserData$(userId) which returns user data based on dropdown select and if not provided with userId returns a default stream,
pauser reflects pause/play status,
var clicks = Observable.fromEvent(document, 'click')
.map(function() {
var pauseStatus= state.get('dashboard').get('pauseStatus')
return pauseStatus;
});
var pauser = new Subject();
clicks.subscribe(pauser);
I want to toggle between different streams and make them pausable as well:
var sources = [];
var newSub =()=>{return new Subject();}
sources.push(newSub());
getUserData$().subscribe(sources[0]);
//if new user is selected from dropdown
if(user_id){
sources.push(newSub());
getUserData$(user_id).subscribe(sources[sources.length-1]);
}
var pausable = pauser.switchMap(paused => paused ? Observable.empty() : sources[sources.length-1]);
pausable.subscribe(function(result){...})
but it keeps jumping between two streams rendering both ,what I am doing wrong?

redux observable map not invoked

I have this code, and failing to understand why I am not getting inside the map function (where I have the comment "I AM NEVER GETTING TO THIS PART OF THE CODE"):
export const fiveCPMonitoringLoadEpic = (action$, store) =>
action$
.ofType(
FIVE_CP_MONITORING_ACTION_TYPES.LOAD_FIVE_CP_MONITORING_DATA_STARTED
)
.debounceTime(250)
.switchMap(action => {
const params = action.params;
const siteId = { params };
// getting site's EDC accounts (observable):
const siteEdcAccount$ = getSiteEDCAccountsObservable(params);
const result$ = siteEdcAccount$.map(edcResponse => {
// getting here - all good so far.
const edcAccount = edcResponse[0];
// creating another observable (from promise - nothing special)
const fiveCPMonitoringEvent$ = getFiveCPAndTransmissionEventsObservable(
{
...params,
edcAccountId: edcAccount.utilityAccountNumber
}
);
fiveCPMonitoringEvent$.subscribe(x => {
// this is working... I am getting to this part of the code
// --------------------------------------------------------
console.log(x);
console.log('I am getting this printed out as expected');
});
return fiveCPMonitoringEvent$.map(events => {
// I NEVER GET TO THIS PART!!!!!
// -----------------------------
console.log('----- forecast-----');
// according to response - request the prediction (from the event start time if ACTIVE event exists, or from current time if no active event)
const activeEvent = DrEventUtils.getActiveEvent(events);
if (activeEvent) {
// get event start time
const startTime = activeEvent.startTime;
// return getPredictionMeasurementsObservable({...params, startTime}
const predictions = getPredictionMock(startTime - 300);
return Observable.of(predictions).delay(Math.random() * 2000);
} else {
// return getPredictionMeasurementsObservable({...params}
const predictions = getPredictionMock(
DateUtils.getLocalDateInUtcSeconds(new Date().getTime())
);
return Observable.of(predictions).delay(Math.random() * 2000);
}
});
can someone please shed some light here?
why when using subscribe it is working, but when using map on the observable it is not?
isn't map suppose to be invoked every time the observable fires?
Thanks,
Jim.
Until you subscribe to your observable, it is cold and does not emit values. Once you subscribe to it, the map will be invoked. This is a feature of rxjs meant to avoid operations that make no change (= no cunsumer uses the values). There are numerous blog posts on the subject, search 'cold vs hot obserables' on google

RxJS Emitting Subscribe Twice

I have a RXJS function that will create an empty Observable, tap into the result and return that new observable. I want the observable to always run the tap so I noop subscribe (in the real case it might not ever be subscribed to).
function that() {
let obs = of({});
obs = obs.pipe(tap(() => console.log('here')))
obs.subscribe();
return obs;
}
const res = that();
res.subscribe(() => console.log('finished'))
If you run this code on StackBlitz, you will notice that here is fired twice. The output looks like this:
here
here
finished
I've tried several different approaches but I can't ever seem to get it to work where it doesn't emit twice.
You subscribe TWICE:
function that() {
let obs = of({});
obs = obs.pipe(tap(() => console.log('here')))
obs.subscribe(); // first subscription
return obs;
}
const res = that();
res.subscribe(() => console.log('finished')) // second subscription
This is the same observable you subscribe to, once in the function, then on the returned value.
Just don't subscribe in the function
function that() {
let obs = of({});
obs = obs.pipe(tap(() => console.log('here')))
return obs;
}
const res = that();
res.subscribe(() => console.log('finished')) // subscribe from here only
See the updated StackBlitz.
Is it just a case of only tapping only the inner subscription?
function that() {
let obs = of({});
obs.pipe(tap(() => console.log('here'))).subscribe();
return obs;
}
const res = that();
res.subscribe(() => console.log('finished'))

Resources