javascriptangularangular-materialmat-autocomplete

Infinite scroll in mat-autocomplete angular 11


Please don't mark it as duplicate

I'm new in angular material design and I have a problem with mat-autocomplete. I have multiple Mat-Autocomplete in FormArray of FromGroup. On keyup in the input field, it's getting data from API calls and filled the autocomplete. After getting data on Keyup it will open the panel.

  1. when I press word then a list of autocomplete opens then I want this list as infinite-scroll
  2. I have multiple autocomplete in formArray of formGroup.

I don't want to use third-party dependency in the project like ngx-infinite-scroll.

enter image description here


Solution

  • Working Demo in this Stackblitz Link

    When you want to detect autocomplete scroll end position you can use custom directive. In this directive you can calculate position of panel control and detect scroll end position, and once scroll end detected you can emit event to component. Directive Name is mat-autocomplete[optionsScroll] so that it auto detects mat-autocomplete component with optionScroll event and this custom directive is applied to all of this matching component. Directive is as follow..

    export interface IAutoCompleteScrollEvent {
      autoComplete: MatAutocomplete;
      scrollEvent: Event;
    }
    
    @Directive({
      selector: 'mat-autocomplete[optionsScroll]',
      exportAs: 'mat-autocomplete[optionsScroll]'
    })
    export class MatAutocompleteOptionsScrollDirective {
       @Input() thresholdPercent = 0.8;
       @Output('optionsScroll') scroll = new EventEmitter<IAutoCompleteScrollEvent>();
      _onDestroy = new Subject();
      constructor(public autoComplete: MatAutocomplete) {
         this.autoComplete.opened
        .pipe(
          tap(() => {
          // Note: When autocomplete raises opened, panel is not yet created (by Overlay)
          // Note: The panel will be available on next tick
          // Note: The panel wil NOT open if there are no options to display
          setTimeout(() => {
            // Note: remove listner just for safety, in case the close event is skipped.
            this.removeScrollEventListener();
            this.autoComplete.panel.nativeElement.addEventListener(
              'scroll',
              this.onScroll.bind(this)
            );
          }, 5000);
        }),
        takeUntil(this._onDestroy)
      )
      .subscribe();
    
    this.autoComplete.closed
      .pipe(
        tap(() => this.removeScrollEventListener()),
        takeUntil(this._onDestroy)
      )
      .subscribe();
    }
    
     private removeScrollEventListener() {
      if (this.autoComplete?.panel) {
       this.autoComplete.panel.nativeElement.removeEventListener(
        'scroll',
        this.onScroll
       );
     }
    }
    
     ngOnDestroy() {
       this._onDestroy.next();
       this._onDestroy.complete();
    
       this.removeScrollEventListener();
     }
    
     onScroll(event: Event) {
       if (this.thresholdPercent === undefined) {
         console.log('undefined');
         this.scroll.next({ autoComplete: this.autoComplete, scrollEvent: event });
       } else {
         const scrollTop = (event.target as HTMLElement).scrollTop;
         const scrollHeight = (event.target as HTMLElement).scrollHeight;
         const elementHeight = (event.target as HTMLElement).clientHeight;
         const atBottom = scrollHeight === scrollTop + elementHeight;
       if (atBottom) {
          this.scroll.next();
       }
      }
     }
    }
    

    Now, you have to call scroll event to mat-autocomplete. On every scroll end onScroll() event is called by our directive.

    <mat-autocomplete (optionsScroll)="onScroll()" > ... </mat-autocomplete>
    

    Now, You have to load first and next chunk of data to mat-autocomplete like this..

      weightData$ = this.startSearch$.pipe(
          startWith(''),
          debounceTime(200),
          switchMap(filter => {
             //Note: Reset the page with every new seach text
             let currentPage = 1;
             return this.next$.pipe(
                startWith(currentPage),
                  //Note: Until the backend responds, ignore NextPage requests.
                exhaustMap(_ => this.getProducts(String(filter), currentPage)),
                tap(() => currentPage++),
                  //Note: This is a custom operator because we also need the last emitted value.
                 //Note: Stop if there are no more pages, or no results at all for the current search text.
                takeWhileInclusive((p: any) => p.length > 0),
                scan((allProducts: any, newProducts: any) => allProducts.concat(newProducts), [] ) );
                })
              );
       
      private getProducts(startsWith: string, page: number): Observable<any[]> {
      
       const take = 6;
       const skip = page > 0 ? (page - 1) * take : 0;
    
       const filtered = this.weightData.filter(option => String(option).toLowerCase().startsWith(startsWith.toLowerCase()));
    
       return of(filtered.slice(skip, skip + take));
      }
      onScroll() {
         this.next$.next();
      }
    

    So at first time we load only first chunk of data, when we reached end of scroll then we again emit event using next$ subject and our stream weightData$ is rerun and gives us appropiate output.