Reactivejs autocomplete pipeline

I am going through several Reactivejs tutorials and I am implementing autocomplete using rx.js.

It looks great, but I'm wondering how to achieve some functionality. Look at this code:

const $title = $('#title'); // input element
const $results = $('#result');

Rx.Observable.fromEvent($title, 'keyup')
    .map(event => event.target.value.trim())
    .distinctUntilChanged()
    .debounceTime(500)
    .switchMap(getItems) // getItems sends request for result based on input value and returns promise
    .subscribe(items => {
        $results.empty();
        $results.append(items.map(record => $('<li> /').text(record)));
    });

      

This works great, I can get an updated list when the input value changes. But if the user has completely cleared the login, I want to prevent the request from being sent. I can achieve this with a filter:

Rx.Observable.fromEvent($title, 'keyup')
    .map(event => event.target.value.trim())
    .filter(query => query.length > 0)
    .distinctUntilChanged()
    .debounceTime(500)
    .switchMap(getItems)
    .subscribe(items => {
        $results.empty();
        $results.append(items.map(record => $('<li> /').text(record)));
    });

      

but in case the user has cleared this entry, I don't want to send an additional request and I want to load the default list.

How can I achieve this?

Is there a way to grab a new record in an observable collection, and in case it is an empty string that breaks the processing, but somehow subscribe to it ?:

 Rx.Observable.fromEvent($title, 'keyup')
        .map(event => event.target.value.trim())

        // CHECK IF INPUT VALUE IS EMPTY STRING, NAD IF IT IS
        // PREVENT PIPELINE STEPS BELOW FROM BEING PROCESSED.
        // ALSO SHOW DEFAULT LIST ITEMS

        .distinctUntilChanged()
        .debounceTime(500)
        .switchMap(getItems)
        .subscribe(items => {
            $results.empty();
            $results.append(items.map(record => $('<li> /').text(record)));
        });

      

I tried to implement it like this:

const query$ = Rx.Observable.fromEvent($title, 'keyup')
    .map(event => event.target.value.trim());

query$
    .filter(query => query.length > 0)
    .distinctUntilChanged()
    .debounceTime(500)
    .switchMap(getItems)
    .subscribe(items => {
        $results.empty();
        $results.append(items.map(record => $('<li> /').text(record)));
    });

query$.subscribe(query => {
    if (!query.length) {
        $results.text('default list'); // this list will still be overwritten with last fetch result
    }
});

      

But the last response for the request still calls my subscribe listener and populates the list with the results.

+3


source to share


2 answers


Simplest solution:

Rx.Observable.fromEvent($title, 'keyup')
    .map(event => event.target.value.trim())
    .distinctUntilChanged()
    .debounceTime(500)
    .switchMap((search) => {
        if(!search) return Rx.Observable.of(null);

        return getItems(search);
    })
    .subscribe(items => {
        $results.empty();
        if (items == null) {
            $results.text('default list');
        } else {
            $results.append(items.map(record => $('<li> /').text(record)));
        }
    });

      

Explanation

All the magic is in the operator switchMap

. It basically replaces the observable source with the one provided by the callback. When the next event reaches switchMap

, it ends the previously created observable and creates a new one.



In your case, it used to query the server for user input and to cancel the previous request if the user changes input while [the query] is still in progress.

The problem with your implementation is that switchMap

it doesn't know anything about empty inputs. Chain of events:

  • User
  • presses 'a'
    • debwnments of the first thread starts downloading items from the server
    • the second thread ignores the event
  • user clears input
    • the first thread filters the event with filter

      (event doesn't reach switchMap

      )
    • the second thread shows the default items
  • server responses with elements and first thread subscription logic. This way the items are overwritten by default.
+2


source


You can create one shared observable source, share it, and then create two subscriptions - one for an empty result and one for a non-empty result. You can resume the same effect function that is called in the subscription. It will remove ifs in your code and make it more readable and reusable.

Something like that:



let input = document.getElementById('inp');
let obs$ = Rx.Observable.fromEvent(input, 'keyup')
  .map(event => event.target.value.trim())
  .share();

function doEffect(result) {
  console.log(result)
}

obs$
  .filter(query => query.length > 0)
  .switchMap(x=>Rx.Observable.of([x.toUpperCase(), x.toUpperCase()]))
  .subscribe(x=>doEffect(x))

obs$
  .filter(query => query.length === 0)
  .mapTo([])
  .subscribe(x=>doSideEffect(x))

      

0


source







All Articles