0

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

neilnikkunilesh
  • 363
  • 2
  • 13
  • when you press word then list of autocomplete opens then you want this list as infinite-scroll ? – Developer Jun 09 '21 at 11:39
  • yes. @GaurangDhorda – neilnikkunilesh Jun 09 '21 at 11:40
  • Data coming from api ? if Yes then your server api is supporting pagination to load more data from server for mat-autocomplete ? – Developer Jun 09 '21 at 11:45
  • Yes, data coming from API. Its supporting the pagination. But the problem here is how can I call that method which getting more data on infinite scroll? – neilnikkunilesh Jun 09 '21 at 11:48
  • 3
    https://stackblitz.com/edit/angular-nxjnph?file=src%2Fapp%2Fautocomplete-overview-example.ts here is your example – Developer Jun 09 '21 at 13:11
  • Thanks for the help. But in this example its loads all the data once then slicing it on the scroll of the panel. – neilnikkunilesh Jun 09 '21 at 13:38
  • You can use your api for initial load of data and then load next chunk of your api data too! and if you see it will default limits only 6 records to list auto complelte. `const take = 6;` see this inside getData function. – Developer Jun 09 '21 at 14:13
  • In my case, I can't keep the `this.filteredStates = this.stateCtrl.valueChanges.pipe()` inside the `constructor` because I have Mat-Table with the rows FormArray of FormGroup in that so I have multiple `stateCtrl`. I'm accessing the formControl using position `at(i)` – neilnikkunilesh Jun 10 '21 at 05:27
  • @GaurangDhorda this is how I'm getting fromControl `StateForm.get('StateRows').at(i).get('stateNumber').valueChanges.pipe()` that's why I can't keep it in `contuctor()` – neilnikkunilesh Jun 10 '21 at 06:07
  • In this case you have to add your code first. and then one can help you more. add your code demo in stackblitz and share here. – Developer Jun 10 '21 at 06:55
  • @GaurangDhorda here is the Demo example link [link](https://stackblitz.com/edit/angular-matautocomplte-infinite-scroll?file=src/app/table-basic-example.ts) In the weight column I have applied the mat-autocomplete suggestion box. On key hit it will call the method `SearchWeightuggestions()` and after some suggestion, I want to load more data on the scroll of the panel. No more than 10 on once scroll. – neilnikkunilesh Jun 10 '21 at 07:20
  • 1
    https://stackblitz.com/edit/angular-matautocomplte-infinite-scroll-prnixm?file=src%2Fapp%2Ftable-basic-example.ts check this out @neilnikkunilesh – Developer Jun 10 '21 at 11:58
  • Thanks, @GaurangDhorda for making things worked. – neilnikkunilesh Jun 10 '21 at 12:28

1 Answers1

3

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.

Developer
  • 3,309
  • 2
  • 19
  • 23