Collectives™ on Stack Overflow
Find centralized, trusted content and collaborate around the technologies you use most.
Learn more about Collectives
Teams
Q&A for work
Connect and share knowledge within a single location that is structured and easy to search.
Learn more about Teams
I have two separate but overlapping user interaction flows that both trigger the same action. If one flow is started, the other one should not start until the first is complete. I'm probably overthinking this but I can't find a pattern that does what I want.
Concrete example:
I want to open a context menu for a selection in a text area with two separate interaction flows:
Via Mouse: Mouse Down -> Selection Changed -> Mouse Up
Via Keyboard: Selection Changed -> Timeout(500ms)
As you can see, both flows overlap in the "Selection Changed" part, but the first and final conditions are different. When selecting with the keyboard, there is no definite end condition, so I rely on a timer. When selecting with the mouse there
is
a definite end condition (Mouse Up), so I want to show the menu immediately. Both are defined as separate Observables that complete on their end condition.
To differentiate between the two, I want to wait for whichever observable emits first and then only listen to that observable until it completes and then start again. So when there is a Mouse Down event I want to wait for the Mouse Up event to trigger. When there is a selection change without the Mouse Down, I start the timeout.
Things I tried:
exhaust/exhaustMap:
That is technically what I want (follow one Observable to the end before considering the next) but those don't take a list of Observables but a function that emits observables. My observables are fixed though. I don't know how to wire this up to do what I want.
race:
I can pass in a list of observables and whatever emits first is then followed through until the end. However, it won't resubscribe when it completes.
race()
simply completes and that's that. I tried something like this:
const openMenu$ = of(true).pipe(
startWith(race(a$, b$)),
exhaust(),
repeat()
const openMenu$ = of(true).pipe(
exhaustMap(() => race(a$, b$)),
exhaust(),
repeat()
const openMenu$ = race(a$, b$).pipe(
repeat()
but all of these just repeat the first Observable over and over. I found no way to restart the race.
Any ideas? Or am I going at this completely wrong?
–
A simple race
should be repeatable.
import { defer, timer, race } from 'rxjs';
import { map } from 'rxjs/operators';
// an observable that will emit `0` after a random time less than 3 seconds
const randomTime$ = defer(() => timer(Math.random() * 3000));
// Will randomly emit "a" after a random time less than 3 seconds
const a$ = randomTime$.pipe(map(() => 'a'));
// Will randomly emit "b" after a random time less than 3 seconds
const b$ = randomTime$.pipe(map(() => 'b'));
// Race a$ and b$
const source$ = race(a$, b$).pipe(
// repeat the race 5 times.
random(5)
source$.subscribe(console.log);
The startWith
and exhaust
are unnecessary in the code you have above. race(a$, b$).pipe(repeat())
should be all you need.
I would check that the first observable isn't always emitting before the second one. Perhaps it has it's own startWith
or some sort of synchronous emission that guarantees it "wins" the race. If you race two synchronous observables, the first one always wins.
–
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.