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

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.

Related

Why mapTo changes only one time?

I'm making a stopwatch and when I wanna reset the clock for the second time, it is not changed.
On click at the first time, it sets h: 0, m: 0, s: 0. But when click again, it doesn't set h: 0, m: 0, s: 0 and stopwatch goes ahead.
const events$ = merge(
fromEvent(startBtn, 'click').pipe(mapTo({count: true})),
click$.pipe(mapTo({count: false})),
fromEvent(resetBtn, 'click').pipe(mapTo({time: {h: 0, m: 0, s: 0}})) // there is reseting
)
const stopWatch$ = events$.pipe(
startWith({count: false, time: {h: 0, m: 0, s: 0}}),
scan((state, curr) => (Object.assign(Object.assign({}, state), curr)), {}),
switchMap((state) => state.count
? interval(1000)
.pipe(
tap(_ => {
if (state.time.s > 59) {
state.time.s = 0
state.time.m++
}
if (state.time.s > 59) {
state.time.s = 0
state.time.h++
}
const {h, m, s} = state.time
secondsField.innerHTML = s + 1
minuitesField.innerHTML = m
hours.innerHTML = h
state.time.s++
}),
)
: EMPTY)
stopWatch$.subscribe()
The Problem
You're using mutable state and updating it as a side-effect of events being emitted by observable (That's what tap does).
In general, it's a bad idea to create side effects that indirectly alter the stream they're created in. So creating a log or displaying a value are unlikely to cause issues, but mutating an object and then injecting it back the stream is difficult to maintain/scale.
A sort-of-fix:
Create a new object.
// fromEvent(resetBtn, 'click').pipe(mapTo({time: {h: 0, m: 0, s: 0}}))
fromEvent(resetBtn, 'click').pipe(map(_ => ({time: {h: 0, m: 0, s: 0}})))
That should work, though it's admittedly a band-aid solution.
A Pre-fab Solution
Here's a stopwatch I made a while ago. Here's how it works. You create a stopwatch by giving it a control$ observable (I use a Subject called controller in this example).
When control$ emits "START", the stopWatch starts, when it emits "STOP", the stopwatch stops, and when it emits "RESET" the stopwatch sets the counter back to zero. When control$ errors, completes, or emits "END", the stopwatch errors or completes.
function createStopwatch(control$: Observable<string>, interval = 1000): Observable<number>{
return defer(() => {
let toggle: boolean = false;
let count: number = 0;
const ticker = () => {
return timer(0, interval).pipe(
map(x => count++)
)
}
return control$.pipe(
catchError(_ => of("END")),
s => concat(s, of("END")),
filter(control =>
control === "START" ||
control === "STOP" ||
control === "RESET" ||
control === "END"
),
switchMap(control => {
if(control === "START" && !toggle){
toggle = true;
return ticker();
}else if(control === "STOP" && toggle){
toggle = false;
return EMPTY;
}else if(control === "RESET"){
count = 0;
if(toggle){
return ticker();
}
}
return EMPTY;
})
);
});
}
// Adapted to your code :)
const controller = new Subject<string>();
const seconds$ = createStopwatch(controller);
fromEvent(startBtn, 'click').pipe(mapTo("START")).subscribe(controller);
fromEvent(resetBtn, 'click').pipe(mapTo("RESET")).subscribe(controller);
seconds$.subscribe(seconds => {
secondsField.innerHTML = seconds % 60;
minuitesField.innerHTML = Math.floor(seconds / 60) % 60;
hours.innerHTML = Math.floor(seconds / 3600);
});
As a bonus, you can probably see how you might make a button that Stops this timer without resetting it.
Without a Subject
Here's an even more idiomatically reactive way to do this. It makes a control$ for the stopwatch by merging DOM events directly (No Subject in the middle).
This does take away your ability to write something like controller.next("RESET"); to inject your own value into the stream at will. OR controller.complete(); when your app is done with the stopwatch (Though you might do that automatically through some other event instead).
...
// Adapted to your code :)
createStopwatch(merge(
fromEvent(startBtn, 'click').pipe(mapTo("START")),
fromEvent(resetBtn, 'click').pipe(mapTo("RESET"))
)).subscribe(seconds => {
secondsField.innerHTML = seconds % 60;
minuitesField.innerHTML = Math.floor(seconds / 60) % 60;
hours.innerHTML = Math.floor(seconds / 3600);
});

How to use rxjs buffer with takeWhile

I am working on webrtc. The application sends icecandidates to backend firestore server.
The problem is the call to signaling server is made multiple times as onicecandidate is triggered multiple time. I want collect all the icecandidates and make a single call to signaling server.
The idea is to buffer all the events untill iceGathering is finished. This below attempt does not work
this.pc = new RTCPeerConnection(iceServers);
const source: Observable<any> = fromEvent(this.pc, 'icecandidate');
const takeWhile$ = source
.pipe(
takeWhile(val=> val.currentTarget.iceGatheringState === 'gathering'
))
const buff = source.pipe(buffer(takeWhile$));
buff.subscribe(() => {
// this.pc.onicecandidate = onicecandidateCallback;
})
Method 1:
You are almost there.
The takeWhile$ takes values and emits them while condition is met. So in buff, whenever takeWhile$ emits a value, buff emits a buffer of icecandidate events.
So you only need to emit one value in takeWhile$.
So what you need is takeLast() operator to only emit the last value.
When you put takeLast(1) in takeWhile$, it only emits last value and in buff, last emitted value leads to creation of buffer of icecandidate events.
this.pc = new RTCPeerConnection(iceServers);
const source: Observable<any> = fromEvent(this.pc, "icecandidate");
const takeWhile$ = source.pipe(
takeWhile(val => val.currentTarget.iceGatheringState === "gathering"),
takeLast(1)
);
const buff = source.pipe(buffer(takeWhile$));
buff.subscribe((bufferValues) => {
// bufferValues has a buffer of icecandidate events
// this.pc.onicecandidate = onicecandidateCallback;
});
You'll have access to buffer of icecandidate events in the subscription as bufferValues in above code.
Method 2:
You can also use reduce operator to achieve same scenario
this.pc = new RTCPeerConnection(iceServers);
const source: Observable<any> = fromEvent(this.pc, "icecandidate");
const takeWhile$ = source.pipe(
takeWhile(val => val.currentTarget.iceGatheringState === "gathering"),
reduce((acc, val) => [...acc,val], [])
);
takeWhile$.subscribe((bufferValues) => {
// bufferValues has a buffer of icecandidate events
// this.pc.onicecandidate = onicecandidateCallback;
})

Updating react-table values after Dragging and Dropping a row in React Redux

I´ve accomplished the react drag and drop functionality into my project so i can reorder a row in a react table´s list. The problem is i have a column named 'Sequence', witch shows me the order of the elements, that i can´t update its values.
Example:
before (the rows are draggable):
Sequence | Name
1 Jack
2 Angel
after ( i need to update the values of Sequence wherea i change their position after dropping a specific draggable row, in this case i dragged Jack at the first position and dropped it at the second position) :
Sequence | Name
1 Angel
2 Jack
React/Redux it´s allowing me to change the index order of this array of elements, without getting the 'A state mutation was detected between dispatches' error message, but is not allowing me to update the Sequence values with a new order values.
This is what i have tried so far:
// within the parent class component
// item is an array of objects from child
UpdateSequence(startIndex, endIndex, item) {
// the state.Data is already an array of object
const result = this.state.Data;
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
// this is working without the mutation state error
this.setState({ Data: result })
let positionDiff = 0;
let direction = null;
let newIndex = 0;
positionDiff = endIndex - startIndex;
if (startIndex > endIndex) {
direction = "up";
}
else if (startIndex < endIndex) {
direction = "down";
}
if (positionDiff !== 0) {
for (var x = 0; x <= Math.abs(positionDiff); x++) {
if (x === 0) {
newIndex = startIndex + positionDiff - x;
this.setState(prevState => ({
Data: {
...prevState.Data,
[prevState.Data[newIndex].Sequence]: Data[newIndex].Sequence + positionDiff
},
}));
}
else {
if (direction === "down") {
newIndex = startIndex + positionDiff - x;
this.setState(prevState => ({
Data: {
...prevState.Data,
[prevState.Data[newIndex].Sequence]: Data[newIndex].Sequence - 1
},
}));
}
else if (direction === "up") {
Data= startIndex + positionDiff + x;
this.setState(prevState => ({
Data: {
...prevState.Data,
[prevState.Data[newIndex].Sequence]: Data[newIndex].Sequence + 1
},
}));
}
}
}
// so when i call save action i am stepping into the 'A state mutation was detected between dispatches' error message.
this.props.actions.saveSequence(this.state.Data)
.then(() => {
this.props.actions.loadData();
})
.catch(error => {
toastr['error'](error, 'error....');
})
}
Calling the action 'saveSequence' whenever i try to update the element of the array, 'Sequence', i am getting the 'A state mutation was detected between dispatches' error message.
Any help will be greatfull! Thank you!
note: The logic applied to reorder the Sequence is ok.
While I don't know redux particularly well, I am noticing that you are directly modifying state, which seems like a likely culprit.
const result = this.state.Data;
const [removed] = result.splice(startIndex, 1);
splice is a destructive method that modifies its input, and its input is a reference to something in this.state.
To demonstrate:
> state = {Data: [1,2,3]}
{ Data: [ 1, 2, 3 ] }
> result = state.Data.splice(0,1)
[ 1 ]
> state
{ Data: [ 2, 3 ] }
Notice that state has been modified. This might be what Redux is detecting, and a general React no-no.
To avoid modifying state, the easy way out is to clone the data you are looking to modify
const result = this.state.Data.slice()
Note that this does a shallow copy, so if Data has non-primitive values, you have to watch out for doing destructive edits on those values too. (Look up deep vs shallow copy if you want to find out more.) However, since you are only reordering things, I believe you're safe.
Well, i figured it out changing this part of code:
//code....
const result = item;
const [removed] = result.splice(startIndex, 1);
// i created a new empty copy of the const 'removed', called 'copy' and update the Sequence property of the array like this below. (this code with the sequence number is just a sample of what i came up to fix it )
let copy;
copy = {
...removed,
Sequence: 1000,
};
result.splice(endIndex, 0, copy);
After i didn´t setState for it, so i commented this line:
// this.setState({ Data: result })
//...code
and the end of it was putting the result to the save action as a parameter , and not the state.
this.props.actions.saveSequence(result)
Works and now i have i fully drag and drop functionality saving the new order sequence into the database with no more 'A state mutation was detected between dispatches' error message!

RXJS take 1 if synchronous, else use defaultValue

I know activatedRoute is actually BehaviourSubject, but since it's exposed API is observable, I don't want to take chances.
activatedRoute.queryParams.pipe(
take(1),
).subscribe(query => { // { page?: '1' | '2' | '3'... }
this.currentPage = new BehaviourSubject(+(query.page) || 1);
// Other components actually change this
});
console.log(this.currentPage); // I need this to not be undefined!
Does a pipe resembling this exists? takeSynchonously(1, { page: 1 })
I have found the answer, it is a combination of takeUntil, timer and queueScheduler.
const { BehaviorSubject, ReplaySubject, Subject, timer, queueScheduler } = rxjs;
const { startWith, defaultIfEmpty, takeUntil, take } = rxjs.operators;
new Subject().pipe(
take(1),
takeUntil(timer(1, queueScheduler)),
defaultIfEmpty(0),
).subscribe(n => console.log('Subject', n));
new ReplaySubject(1).pipe(
take(1),
takeUntil(timer(1, queueScheduler)),
defaultIfEmpty(0),
).subscribe(n => console.log('ReplaySubject', n));
const rs = new ReplaySubject(1);
rs.next(1)
rs.pipe(
take(1),
takeUntil(timer(1, queueScheduler)),
defaultIfEmpty(0),
).subscribe(n => console.log('ReplaySubject (with value)', n));
new BehaviorSubject(5).pipe(
take(1),
takeUntil(timer(1, queueScheduler)),
defaultIfEmpty(0),
).subscribe(n => console.log('BehaviorSubject', n));
console.log('priority')
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.2/rxjs.umd.js"></script>
Note: This must be used in combination with ReplySubject or similar. Anything with initial value will be synchronous, but those without it will be processed in the next tick.

How to ignore new values in Observer during execution

I have some Subject. And one Observer subscribed to it. How to omit all Observer invocations if it is already processing one?
var subject = new Subject();
var observer = {
next: x => {
//... some long processing is here
console.log('Observer got a next value: ' + x)
}
};
subject.subscribe(observer);
subject.next(0);
subject.next(1);// <-- if 0 value is not processed in the observer then skip it
subject.next(2);// <-- if 0 value is not processed in the observer then skip it
I of cause can introduce some flag, set it in Observer before execution and clear it after. And apply filter operator, like this:
var subject = new Subject();
var flag = true;
var observer = {
next: x => {
flag = false;
//... some long processing is here
console.log('Observer got a next value: ' + x)
flag = true;
}
};
subject.filter(() => flag).subscribe(observer);
subject.next(0);
subject.next(1);// <-- if previous value is not processed in the observer then skip it
subject.next(2);// <-- if 0 value is not processed in the observer then skip it
But I believe that exists more elegant and efficient way to achieve that.
Use the exhaustMap operator instead of trying roll your own backpressure. It is designed to ignore new events while waiting for the current one to complete.
const clicks = fromEvent(document, 'click');
const result = clicks.pipe(
exhaustMap((ev) => interval(1000).pipe(take(5))),
);
result.subscribe(x => console.log(x));

Resources