How to code a drag-lock using RxJS? - rxjs

A drag-lock interaction starts with a first double-click, followed by some mouse moves, and ends with a second double-click. It is a variant of the drag-and-drop.
I want to code this interaction in RxJS, but the big problem I face is that the same event (double-click) starts and ends the interaction (a problem we do not have while coding a drag-and-drop).
How to implement a drag-lock using RxJS?
I want to subscribe to the first-click, to all the moves, and the final double-click.

plunker: https://plnkr.co/edit/MXUtPzH9iD40VjVxLBwB?p=preview
Lets define an event - DragLockEvent which consists of type "DragLockEventType" which could be either 'initialClick', 'move' or 'finalClick', and the mouse move event, in case it is of type 'move'.
Now let's create an observable which will emit these events: dragLock$.
Inside the observable there's a flag - "dragging", indicating whether or not we are in the middle of dragging.
Now we need to listen to double click events, and for each such event we need to toggle the dragging flag, and to emit the matching event.
Then, in case we are in dragging mode, we start listening to mouse move events and report them, until we have another double click.
type DragLockEventType = 'initialClick' | 'move' | 'finalClick';
interface DragLockEvent {
type: DragLockEventType;
moveEvent?: MouseEvent;
}
const dragLock$: Observable<DragLockEvent> = Observable.create((observer: Observer<DragLockEvent>) => {
let dragging = false;
const doubleClick$ = fromEvent(document.documentElement, 'dblclick')
.pipe(takeWhile(() => !observer.closed));
const mouseMove$ = fromEvent(document.documentElement, 'mousemove')
.pipe(takeWhile(() => !observer.closed));
doubleClick$
.pipe(
tap(() => {
dragging = !dragging;
if (dragging) observer.next({ type: 'initialClick' });
else observer.next({ type: 'finalClick' });
}),
filter(() => dragging),
switchMap(() => mouseMove$.pipe(takeUntil(doubleClick$)))
)
.subscribe((e: MouseEvent) => observer.next({ type: 'move', moveEvent: e }))
});

Related

How to replace the deprecated repeatWhen(notifier) with repeat(delay)

While testing, refactoring and future-proofing a customers project, I stumbled over this little deprecation notification:
Will be removed in v9 or v10. Use repeat's delay option instead.
repeatWhen(notifier: (notifications: Observable) =>
Observable): MonoTypeOperatorFunction
Simple enough, right? But when I tried, I didn't find a simple way to do so. I have a rough idea how I could hack it. But that's not exactly what I'd like to hand over to a customer as "improved" code. So what obvious path do I fail to see, that leads from this (straight out of the rxjs documentation):
import { of, fromEvent, repeatWhen } from 'rxjs';
const source = of('Repeat message');
const documentClick$ = fromEvent(document, 'click');
***const result = source.pipe(repeatWhen(() => documentClick$));***
result.subscribe(data => console.log(data))
to this:
import { of, fromEvent, repeat } from 'rxjs';
const source = of('Repeat message');
const documentClick$ = fromEvent(document, 'click');
const result = source.pipe(repeat({ delay: ??? () => documentClick$) });
result.subscribe(data => console.log(data))
How to switch an option that accepts a number into an option that repeats whenever the event happens? Well, as said, I have an idea how to achieve it, but it would be incredibly ugly. So what am I missing?
You almost got it right, just remove the question marks :)
source.pipe(repeat({ delay: () => documentClick$ }));
This basically says "Whenever source completes, subscribe to documentClick$ and whenever this (documentClick$) emits, re-subscribe to source.
Did you want to do more with that? I didn't fully understand your last paragraph.
In my little example, where a mouse click is faked by a timer, I get an emission every 2 seconds:
import { of, repeat, timer } from 'rxjs';
const source = of('Repeat message');
const documentClick$ = timer(2000);
const result = source.pipe(repeat({ delay: () => documentClick$ }));
result.subscribe((data) => console.log(data));

Can't understand why async/await isn't working NextJs

I'm looking for a help with how to make a chunk load, when the user scrollbar, and reach a specific div, then it shoul run a function one time, but the code run multiples time:
async function loadMore(){
console.log('i load more');
}
window.addEventListener('scroll', async (event) => {
const {
scrollTop,
scrollHeight,
clientHeight
} = document.documentElement;
if (scrollTop + clientHeight >= scrollHeight - 1 ) {
document.getElementById('final').style.height = '3000px'
let msg = await loadMore()
console.log('i finish')
document.getElementById('final').style.height = '30px'
}
}, {
passive: true
});
return (
<div id='final'>
<Image src="/ajax-loader.gif" width={60} height={60} alt="loader spinner"></Image>
</div>
)
Few things:
You should add the window event listener in a useEffect.
If you add the event listener in the render phase of the component, it will run add a event listener on every render which might be 1 of the reasons why it is running multiple times.
You also need to cleanup the event listener as a cleanup of useEffect else you will again end up with a lot of event listeners
You need to debounce the onScroll handler
Scroll event is triggered several times when you scroll which is something you might not want. So, adding a debounce will help reduce the number of times, the handler is called when a user scrolls.
Lodash's debounce is 1 popular implementation of debounce. You can choose other implementations if you want or create your own one.
import debounce from "lodash.debounce";
function YourComponent() {
async function loadMore(){
console.log('i load more');
}
// "useEffect" so that you don't add a new event listener
// on every render
useEffect(() => {
const onScroll = async (event) => {
// Whatever you want to do when user scrolls
}
// This is the debounced "onScroll" function instance
// "500" specifies that it will be debounced for 500 milliseconds
const debouncedOnScroll = debounce(onScroll, 500);
// Attach the event listener to window
window.addEventListener('scroll', debouncedOnScroll);
// Cleanup the event listener when component unmounts or
// when the "useEffect" runs again.
return () => window.removeEventListener('scroll', debouncedOnScroll);
}, []);
return (
<div id='final'>
{/* Rest of your JSX */}
</div>
)
}
trigger window event listener inside useEffect
make sure to cleanup the event: return () => window.removeEventListener('scroll', callback);

rxjs: cancelling a debounced observable

I have an observable Subject that emits some changes with debouncing:
someSubject.pipe(
debounceTime(5000),
).subscribe(response => {
console.log('Value is', response);
})
Now, I need a Stop button somewhere on the screen that would cancel my debounced emit. So I create a button:
const stopObs = new Subject();
...
<button onClick={() => stopObs.next()}>Stop</button>
and modify my subscription like so:
someSubject.pipe(
debounceTime(5000),
takeUntil(stopObs),
).subscribe(response => {
console.log('Value is', response);
})
This works fine, after hitting "Stop" I stop getting values in console, but there is a problem: the observable is stopped forever. And I need it to be able to emit new values, I only need to cancel already started debounced emits.
My first thought was to create a new subject and use repeatWhen:
const startObs = new Subject();
...
<button onClick={() => startObs.next()}>Start</button>
...
someSubject.pipe(
debounceTime(5000),
takeUntil(stopObs),
repeatWhen(() => startObs)
).subscribe(response => {
console.log('Value is', response);
})
But there's another problem: if I hit "Start" button more than one time and emit more than one value to startObs, then I start getting multiple console.log's for single debounced value!
So is there a way to cancel only debounced emits without stopping the entire observable?
Since debounceTime is just
const duration = timer(dueTime, scheduler);
return debounce(() => duration);
I think you can solve the problem like this:
someSubject.pipe(
debounce(() => timer(5000).pipe(takeUntil(stopObs))),
)
If you want to send the last value when the timer is cancelled due to stopObs, you could try this:
someSubject.pipe(
debounce(
() => timer(5000)
.pipe(
takeUntil(stopObs),
isEmpty(),
)
),
)
isEmpty() will emit true immediately before a complete notification, which is what debounce needs in order to send the last received value. If the timer completes without stopObs's involvement, isEmpty will emit false instead of true, but this still works well for debounce, since it only needs a value from the inner observable.

Handle mousemove stop with RxJs

Is there a way to identify when user stopped moving the mouse? I can't figure it out how to tell in RxJs if user stopped for let's say 2s.
When I use it like this:
fromEvent(document, 'mousemove').pipe(
debounceTime(2000)
).subscribe(() => console.log("Stoped"));
It triggers even if I move cursor out of the screen.
Depends on a situation, but timeout or timeoutWith may work for you.
This example will emit "stop" every time when there was no mousemove events for more than 1s:
const { of, defer, concat, fromEvent } = rxjs;
const { mapTo, timeoutWith, skipUntil } = rxjs.operators;
const move$ = fromEvent(document, 'mousemove').pipe(mapTo('move'));
const moveAndStop$ = move$.pipe(
timeoutWith(
1000,
defer(() => concat(
of('stop'),
moveAndStop$.pipe(skipUntil(move$))
))
)
);
moveAndStop$
.subscribe(e => console.log(e));
<script src="https://unpkg.com/rxjs#6.3.2/bundles/rxjs.umd.min.js"></script>
<div id="app">Move your mouse over me</div>

rxjs: Subscribe each time value is emitted

I want to subscribe to mousemove event each time mouse is clicked down and unsubscribe each time mouse is clicked up.
// start when mousedown
Observable.fromEvent(element, 'mousedown')
.subscribe(() => {
Observable.fromEvent(element, 'mousemove')
// finish when mouseup
.takeUntil(Observable.fromEvent(element, 'mouseup'))
.subscribe(() => {/*do something on mousemove*/});
});
Is it possible to optimize the code (to have single "subscribe" method)?
Observable.fromEvent(element, 'mousedown')
.switchMap(() =>
Observable.fromEvent(element, 'mousemove')
.takeUntil(Observable.fromEvent(element, 'mouseup'))
)
.subscribe(() => {/*do something on mousemove*/});

Resources