Infinite Scroll For Autocomplete In Angular Material 6
I'm trying to implement infinite scroll for autocomplete in angular material 6. My scenario is straightforward, I have an input filed with autocomplete enabled. when the user types
Solution 1:
I know the post is old, but i'm leaving the solution here in case anybody needs it.
The trick is to get the reference to the mat-autocomplete panel's scrollbar. I've done this using a custom directive:
import { Directive, ElementRef, EventEmitter, Input, Output, Host, Self, Optional, AfterViewInit, OnDestroy } from'@angular/core';
import { MatAutocomplete } from'@angular/material';
import { Observable, fromEvent, of, Subject, merge, combineLatest } from'rxjs';
import { map, startWith, switchMap, tap, debounceTime, filter, scan, withLatestFrom, mergeMap, takeUntil, takeWhile, distinctUntilChanged, skipUntil, exhaustMap, endWith } from'rxjs/operators';
import { takeWhileInclusive } from'rxjs-take-while-inclusive';
export interface IAutoCompleteScrollEvent {
autoComplete: MatAutocomplete;
scrollEvent: Event;
}
@Directive({
selector: 'mat-autocomplete[optionsScroll]'
})
exportclassOptionsScrollDirective implements OnDestroy {
@Input() thresholdPercent = .8;
@Output('optionsScroll') scroll = newEventEmitter<IAutoCompleteScrollEvent>();
_onDestroy = newSubject();
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 displaysetTimeout(() => {
// 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))
});
}),
takeUntil(this._onDestroy)).subscribe();
this.autoComplete.closed.pipe(
tap(() =>this.removeScrollEventListener()),
takeUntil(this._onDestroy)).subscribe();
}
private removeScrollEventListener() {
this.autoComplete.panel.nativeElement
.removeEventListener('scroll', this.onScroll);
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
this.removeScrollEventListener();
}
onScroll(event: Event) {
if (this.thresholdPercent === undefined) {
this.scroll.next({ autoComplete: this.autoComplete, scrollEvent: event });
} else {
const threshold = this.thresholdPercent * 100 * event.target.scrollHeight / 100;
const current = event.target.scrollTop + event.target.clientHeight;
//console.log(`scroll ${current}, threshold: ${threshold}`)if (current > threshold) {
//console.log('load next page');this.scroll.next({ autoComplete: this.autoComplete, scrollEvent: event });
}
}
}
}
After this what remains is to load more data from the server when the scrollbar reaches 80% threshold:
import { Component, OnInit } from'@angular/core';
import { FormControl } from'@angular/forms';
import { Observable, fromEvent, of, Subject, merge, combineLatest } from'rxjs';
import { map, startWith, switchMap, tap, debounceTime, filter, scan, withLatestFrom, mergeMap, takeUntil, takeWhile, distinctUntilChanged, skipUntil, exhaustMap, endWith } from'rxjs/operators';
import { MatAutocomplete } from'@angular/material/autocomplete';
import { takeWhileInclusive } from'rxjs-take-while-inclusive';
export interface ILookup {
id: number,
name: string
}
@Component({
selector: 'autocomplete-filter-example',
templateUrl: 'autocomplete-filter-example.html',
styleUrls: ['autocomplete-filter-example.scss'],
})
exportclassAutocompleteFilterExample implements OnInit {
searchText = newFormControl({ id: 2, name: 'ana' });
filteredLookups$: Observable<ILookup[]>;
private lookups: ILookup[] = [];
private nextPage$ = newSubject();
private _onDestroy = newSubject();
// Fake backend api
private getProducts(startsWith: string, page: number): Observable<ILookup[]> {
console.log(`api call filter: ${startsWith}`);
const take = 10;
const skip = page > 0 ? (page - 1) * take : 0;
const filtered = this.lookups
.filter(option => option.name.toLowerCase().startsWith(startsWith.toLowerCase()))
console.log(`skip: ${skip}, take: ${take}`);
returnof(filtered.slice(skip, skip + take));
}
ngOnInit() {
// Note: Generate some mock datathis.lookups = [{ id: 1994, name: 'ana' }, { id: 1989, name: 'narcis' }]
for (let i = 1; i < 100; i++) {
this.lookups.push({ id: i, name: 'test' + i })
}
// Note: listen for search text changesconst filter$ = this.searchText.valueChanges.pipe(
startWith(''),
debounceTime(200),
// Note: If the option valye is bound to object, after selecting the option// Note: the value will change from string to {}. We want to perform search // Note: only when the type is string (no match)filter(q =>typeof q === "string"));
// Note: There are 2 stream here: the search text changes stream and the nextPage$ (raised by directive at 80% scroll)// Note: On every search text change, we issue a backend request starting the first page// Note: While the backend is processing our request we ignore any other NextPage emitts (exhaustMap).// Note: If in this time the search text changes, we don't need those results anymore (switchMap)this.filteredLookups$ = filter$.pipe(
switchMap(filter => {
//Note: Reset the page with every new seach textlet currentPage = 1;
returnthis.nextPage$.pipe(
startWith(currentPage),
//Note: Until the backend responds, ignore NextPage requests.exhaustMap(_ =>this.getProducts(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 => p.length > 0),
scan((allProducts, newProducts) => allProducts.concat(newProducts), []),
);
})); // Note: We let asyncPipe subscribe.
}
displayWith(lookup) {
return lookup ? lookup.name : null;
}
onScroll() {
//Note: This is called multiple times after the scroll has reached the 80% threshold position.this.nextPage$.next();
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
}
Note: I'm using a custom rxjs operator rxjs-take-while-inclusive.
You case see it in action here: DEMO
Post a Comment for "Infinite Scroll For Autocomplete In Angular Material 6"