Thursday, August 31, 2017

rxjs - Discard future returning observables

Leave a Comment

So here is my observable code:

  var suggestions =         Rx.Observable.fromEvent(textInput, 'keyup')           .pluck('target','value')           .filter( (text) => {               text = text.trim();               if (!text.length) // empty input field               {                   this.username_validation_display("empty");               }               else if (!/^\w{1,20}$/.test(text))               {                   this.username_validation_display("invalid");                   return false;               }               return text.length > 0;           })           .debounceTime(300)           .distinctUntilChanged()           .switchMap(term => {               return $.ajax({                 type: "post",                 url: "src/php/search.php",                 data: {                   username: term,                   type: "username"                 }               }).promise();             }           );   suggestions.subscribe(     (r) =>     {         let j = JSON.parse(r);         if (j.length)         {             this.username_validation_display("taken");         }         else         {             this.username_validation_display("valid");         }     },     function (e)     {         console.log(e);     }   ); 

The problem I have is when the input is empty I have a another piece of code that basically returns a 'error: empty input' but it gets overridden by the returning observable. So I was wondering if there was a way to disregard all observables if the text.length is 0, but also re-subscribe when the text length isn't zero.

I've thought about unsubscribe but don't know where to fit it in to try.

3 Answers

Answers 1

Maybe this would work? The idea is to incorporate empty input in the switchmap so the current observable is switched off i.e. discarded also in that case.

  var suggestions =         Rx.Observable.fromEvent(textInput, 'keyup')           .pluck('target','value')           .filter( (text) => {               text = text.trim();               if (!text.length) // empty input field               {                   this.username_validation_display("empty");               }               else if (!/^\w{1,20}$/.test(text))               {                   this.username_validation_display("invalid");                   return false;               }           })           .debounceTime(300)           .distinctUntilChanged()           .switchMap(text=> {               if (text.length > 0) {               return $.ajax({                 type: "post",                 url: "src/php/search.php",                 data: {                   username: text,                   type: "username"                 }               }).promise()}               return Rx.Observable.empty();             }           ); 

Answers 2

I would propose to move the debounceTime call just after the text input stream and do the filtering already on the debounced stream. This way a valid but outdated value will not sneak into an ajax call. Also, we might need to "cancel" an ajax call (or more precisely, ignore its response) when a new, possibly invalid, input happens as we don't want "invalid" status to be overriden with a result of an ongoing asynchronous call. takeUntil can help with that. Note also that Rx.Observable.ajax() can be used instead of $.ajax().

BTW, it might be handy to have a dedicated stream of "validation statuses" so that this.username_validation_display(validationStatus) is done in a single place inside subscribe.

I imagine it to be something like this:

const term$ = Rx.Observable.fromEvent(textInput, "keyup")     .pluck("target", "value")     .map(text => text.trim())     .distinctUntilChanged()     .share();  const suggestion$ = term$     .debounceTime(300)     .filter(term => getValidationError(term).length === 0)     .switchMap(term => Rx.Observable         .ajax({             method: "POST",             url: "src/php/search.php",             body: {                 username: term,                 type: "username"             }         })         .takeUntil(term$)     )     .map(result => JSON.parse(result));  const validationStatus$ = Rx.Observable.merge(     term$.map(getValidationError),     suggestion$.map(getSuggestionStatus));  // "empty", "invalid", "taken", "valid" or "" validationStatus$     .subscribe(validationStatus => this.username_validation_display(validationStatus));  // Helper functions function getValidationError(term) {     if (term.length === 0) {         return "empty";     }      if (!/^\w{1,20}$/.test(term)) {         return "invalid";     }      return ""; }  function getSuggestionStatus(suggestion) {     return suggestion.length > 0 ? "taken" : "valid"; } 

The behavior of the code above would also be different in a sense that between a point in time when a user has typed a valid input and a point when a suggestion has arrived from the server, username_validation_display will have an empty value, i.e. besides "empty", "invalid", "taken" and "valid" statuses there would also be an intermediate empty status which could be replaced with some progress indication in the UI.

Update: I can also think of an alternative approach that should have an equivalent behavior, but corresponding code might look a bit more straightforward:

const username$ = Rx.Observable.fromEvent(textInput, "keyup")     .pluck("target", "value")     .map(text => text.trim())     .distinctUntilChanged();  const validationStatus$ = username$.switchMap(username => {     const validationError = getValidationError(username);     return validationError ?         Rx.Observable.of(validationError) :         Rx.Observable.timer(300)             .switchMapTo(getAvailabilityStatus$(username))             .startWith("pending"); });  // "invalid", "empty", "taken", "valid" or "pending" validationStatus$.subscribe(     validationStatus => this.username_validation_display(validationStatus));  // Helper functions function getAvailabilityStatus$(username) {     return Rx.Observable         .ajax({             method: "POST",             url: "src/php/search.php",             body: {                 username: username,                 type: "username"             }         })         .map(result => JSON.parse(result))         .map(suggestions => suggestions.length > 0 ? "taken" : "valid"); }  function getValidationError(username) {     if (username.length === 0) {         return "empty";     }      if (!/^\w{1,20}$/.test(username)) {         return "invalid";     }      return null; } 

This way we check each distinct input synchronously. If the input is empty or invalid (determined by getValidationError() call), we immediately emit the validation error. Otherwise we immediately emit a "pending" status and start a timer which will spawn an asynchronous username availability check after 300 ms. If a new text arrives before the timer has elapsed or availability status has arrived from the server, we cancel the inner stream and re-start the validation, thanks to switchMap. So it should work pretty much the same as with debounceTime.

Answers 3

You can use your original values streams as notifier for your ajax call to abort its value. This should give you enough control to add aditional handling for showing messages if the field is empty without it being overwritten by the previous ajax results.

const inputValues = Rx.Observable.fromEvent(document.getElementById("inputField"), "keyup")    .pluck("target","value")    .distinctUntilChanged()    .do(val => console.log('new value: ' + val));      inputValues    .filter(val => val.length > 0)    .switchMap(text => doAjaxCall(text)      .takeUntil(inputValues)    )    .subscribe(val => console.log('received a value: ' + val));      function doAjaxCall(inputValue) {    return Rx.Observable.of(inputValue)      .do(val => console.log('ajax call starting for: ' + val))      .delay(1000)      .do(val => console.log('ajax call returned for: ' + val));  }
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.4.2/Rx.js"></script>  <label>input field<input type="text" id="inputField" /></label>

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment