user2689931
user2689931

Reputation: 383

Tricky steam observable merge in rxjs

I could use a bit of help. I'm trying to an experiment where I start my ajax request on mouseenters of my button, but only consume the result of that request if the user clicks (or if the request hasn't finished yet when the user clicks, consume the result as soon as it does). If the user leaves the button without clicking, the request should be cancelled.

Where I'm stuck is how to merge the click stream with the request stream. I cannot use withLatestFrom, because then if the request finishes after the click, it will not be consumed. I also cannot use combineLatest, because then if any click has occurred in the past, the the request will be consumed, even if I'm currently just mousing over. Would love some guidance. It's been a fun problem to think about but I'm stuck

const fetchContent = (url) => {
  const timeDelay$ = Rx.Observable.timer(1000); // simulating a slow request
  const request$ =  Rx.Observable.create(observer =>
    fetch(url, { mode: 'no-cors' })
      .then(json => {
         observer.onNext('res')
         observer.onCompleted()
      })
      .catch(e => observer.onError())
  )
  return timeDelay$.concat(request$)
}
const hover$ = Rx.Observable.fromEvent(myButton, 'mouseenter')
const leave$ = Rx.Observable.fromEvent(myButton, 'mouseleave')
const click$ = Rx.Observable.fromEvent(myButton, 'click')

const hoverRequest$ = hover$
  .flatMap(e =>
    fetchContent(e.target.getAttribute('href'))
      .takeUntil(leave$.takeUntil(click$))
  )

const displayData$ = click$
  .combineLatest(hoverRequest$)

displayData$.subscribe(x => console.log(x))

Upvotes: 1

Views: 718

Answers (2)

paulpdaniels
paulpdaniels

Reputation: 18663

You aren't terribly far off actually. You are just missing the inclusion of zip really. Since what you really need for propagation is for both a mouse click and the request to complete. By zipping the request and the mouse click event you can make sure that neither emits without the other.

const hover$ = Rx.Observable.fromEvent(myButton, 'mouseenter');
const leave$ = Rx.Observable.fromEvent(myButton, 'mouseleave');
const click$ = Rx.Observable.fromEvent(myButton, 'click');


//Make sure only the latest hover is emitting requests
hover$.flatMapLatest(() => {

  //The content request
  const pending$ = fetchContent();

  //Only cancel on leave if no click has been made
  const canceler$ = leave$.takeUntil(click$);

  //Combine the request result and click event so they wait for each other
  return Rx.Observable.zip(pending$, click$, (res, _) => res)

               //Only need the first emission
               .take(1)

               //Cancel early if the user leaves the button
               .takeUntil(canceler$);

});

Upvotes: 2

user3743222
user3743222

Reputation: 18665

Maybe you could conceptualize this as three events (hover, leave, click as you call them) triggering three actions (emit request, cancel request, pass request result) modifying a state (request? pass request?).

Now this is done in a rush on a sunday evening, but with a little bit of luck , something like this could work :

function label(str) {return function(x){var obj = {}; obj[str] = x; return obj;}}
function getLabel(obj) {return Object.keys(obj)[0];}
const hover$ = Rx.Observable.fromEvent(myButton, 'mouseenter').map(label('hover'));
const leave$ = Rx.Observable.fromEvent(myButton, 'mouseleave').map(label('leave'));
const click$ = Rx.Observable.fromEvent(myButton, 'click').map(label('click'));

var initialState = {request : undefined, response : undefined, passResponse : false};

var displayData$ = Rx.Observable.merge(hover$, leave$, click$)
  .scan(function (state, intent){
          switch (getLabel(intent)) {
            case 'hover' : 
              if (!state.request) {
                state.request = someRequest;
                state.response$ = Rx.Observable.fromPromise(executeRequest(someRequest));
              }
            break;
            case 'leave' : 
              if (state.request && !state.passResponse) cancelRequest(someRequest);
              state.passResponse = false;
            break;
            case 'click' :
              if (!state.request) {
                state.response$ = Rx.Observable.fromPromise(executeRequest(someRequest));
              }
              state.passResponse = true;
          }
        }, initial_state)
  .filter(function (state){return state.passResponse;})
  .pluck('response$')
  .concatAll()

Upvotes: 1

Related Questions