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.