akcasoy
akcasoy

Reputation: 7225

How to make an async autocomplete in Angular 6 (rxjs 6), which does not show any irrelevant data by fast typing

What i have now: I have a custom autocomplete implementation, looks like this in html:

<input [formControl]="parentFormControl"
       (mouseup)="autocomplete()"
       (input)="autocomplete()">

<ul *ngIf="showAutocompleteList">
  <li *ngFor="let listItem of autocompleteList; let index = index;">
    <a role="option" href="#" type="button" (mousedown)="select(listItem)" [innerHtml]="highlight(listItem)"></a>
  </li>
</ul>

parentFormControl is the value behind the input (the modal so to speak), and is a FormControl. So this encapsulates the text which is displayed in input, and submitted with the submit button.

Now everytime i type or click in input, the function autocomplete is called, and makes a normal async call with subscribe:

  autocomplete(): void {
    if (this.autocompleteSubscription) {
      this.autocompleteSubscription.unsubscribe();
    }
    this.autocompleteSubscription = this.http.get('url').subscribe(
      (autocompleteList:  Array<OptionItem>) => {
        this.autocompleteList = autocompleteList;
      }
    );
  }

I actually do not have any delay for the aysnc call, so everytime user types, I call the backend immediately. But imagine that user types 'Software', I of course do not want to display any irrelevant results. For example we typed 'Soft', and the XHR call is fired. We continue typing and typed 'Softw', another XHR call is fired, before the first call finished its job. So i actually immediately cancel the first execution, since it is not relevant anymore. That's why everytime the autocomplete function is called, i call the unsubscribe of the previous subscription.

Question: Is this the way i should implement my autocomplete behaviour? I have seen tooday an artical about the exact same thing, but they implement this behaviour with a detailed (or pure?) rxjs, with Pipes. this is the link: https://blog.strongbrew.io/building-a-safe-autocomplete-operator-with-rxjs/

I have also seen other examples which are pipes, but they also have in their html for me a bit foreign syntax like '{{ text$ | async }}'.

So i wanted to be sure, whether what i have now (subscribe & unsubcribe) is a good way of achieving what I want? Or should i really change the implementation and implement sth like what they have? Which side has which pros and cons?

And if i should switch my implementation, can you provide an example code with my formControl as model?

Thanks in advance

Edit:

I now have sth like this:

  ngOnInit(): void {
    this.parentFormControl.valueChanges
      .pipe(
        debounceTime(1000),
        distinctUntilChanged(),
        filter(value => this.myGetRequestCondition() ? true : false),
        switchMap((value: string) =>  {
            return this.http.get('url' + value)
              .pipe(catchError(err => {
                 this.myErrorFunction(err);
                 return EMPTY;
              }));
        }))
      .subscribe(autocompleteList => {
        this.autocompleteList = autocompleteList;
      });
  }

Upvotes: 1

Views: 4798

Answers (2)

Vikas
Vikas

Reputation: 12036

Reactive form instances like FormGroup and FormControl have a valueChages method that returns an observable that emits the latest values. You can therefore subscribe to valueChanges to update instance variables or perform operations.

So i wanted to be sure, whether what i have now (subscribe & unsubcribe) is a good way of achieving what I want?

There is not anything wrong with your approach but working with observable will let you use RxJs Operators which makes your job easier when it comes to maintaing,testing and what not.

valueChanges returns an observable, the sky is pretty much the limit in terms of what you can do with the values that are emitted.

Error Handling.

Whenever Value of formControl Changes switchMap() creates a brand new ajax observable.if you get a 404 from your server the error is propagated from inner to outer Chain and whole Streams Die.

To keep it alive you need to shield the main observerchain. and to isolate the main observer chain use catchError() operator with switchMap() .

switchMap() does not care if inner Observable has completed it will only complete when outer Observable has completed. Even though inner Observable chain dies,outer Observable chain stays alive because error handler has not been called for outer Observable chain.

Apart from this if your input Control is filled and again emptied formcontrol will pickup the changes and send a request even though its empty you can take advantage of filter operator which Emits values that pass the provided condition and distinctUntilChanged which Only emits when the current value is different than the last.

Here is the Solution.:

  ngOnInit(): void {
        this.parentFormControl.valueChanges
          .pipe(
            debounceTime(1000),
            distinctUntilChanged(),
            filter(value=>value? true: false),
            switchMap((value)=>this.http.get('url' + value).pipe(catchError(() => {return empty()}))
            ))
          .subscribe(autocompleteList => {
            this.autocomplete= autocompleteList;
          });
      }

Live Demo

Upvotes: 2

Sandip Jaiswal
Sandip Jaiswal

Reputation: 3730

If you are using ReactiveForms then the best way is to watch valuechange of formcontrol.

this.parentFormControl.valueChanges.pipe(
      switchMap((value) => this.http.get('url').pipe(catchError(err => of([]))))
    ).subscribe(autocompleteList => this.autocompleteList = autocompleteList);

when the source Observable emits switchMap cancel any previous subscriptions of the inner Observable. Whenever a new value will be type by user then switchMap will cancel its inner subscription (network call in this case).

According to your new code this will be solution.

ngOnInit(): void {
    this.parentFormControl.valueChanges
      .pipe(
        debounceTime(1000),
        switchMap((value: string) =>  {
          if (this.condition()) {
            return this.http.get('url' + value).pipe(catchError(err => of([])));
          }
          return of([]); // or of(this.autocompleteList)
        }))
      .subscribe(autocompleteList => {
        this.autocompleteList = autocompleteList;
      });
  }

Upvotes: 2

Related Questions