I recently started modifying a project based on Angular 6 and ran into a bit of an issue. So, I have (lets assume) 4 Mat-Select fields, for Language, Currency, Country and Organization.
Initially all the dropdowns have 10 default values, I fetch by making an API call. My requirement is to make another API call once the user scrolls to the end of the Mat-Options box that opens up on selecting dropdown.
I had referred to This Question and it works fine but with a few issue, I have noticed. That answer covers the solution for one field. Do we have to repeat the code if we are using multiple fields?
Here is the Html
<mat-select [disabled]="isDisabled" [(ngModel)]="model.currency" name="currency" (selectionChange)="OnChange('currency',$event.value)" #currency>
<mat-option [value]="getCurrency.currencyCode" *ngFor="let getCurrency of fetchCurrency |
filternew:searchCurrencyvaluedrop">{{ getCurrency.currencyDescription }}
</mat-option>
</mat-select>
<mat-select [disabled]="isDisabled" [(ngModel)]="model.language" name="language"
(selectionChange)="OnChange('language', $event.value)" #language>
<mat-option [value]="getLanguage.languageDescription" *ngFor="let getLanguage of fetchLanguage |
filternew:searchLanguagevaluedrop">{{ getLanguage.languageDescription }}</mat-option>
</mat-select>
I am adding only two fields for the sake of simplicity. Below is the code from .ts:
viewIndex = 0;
windowSize = 10;
private readonly PIXEL_TOLERANCE = 3.0;
ngAfterViewInit() {
this.selectElemCu.openedChange.subscribe((event) =>
this.registerPanelScrollEventCurrency(event)
);
this.selectElem.openedChange.subscribe((event) =>
this.registerPanelScrollEventLang(event)
);
}
//for language field
registerPanelScrollEventLang(e) {
if (e) {
const panel = this.selectElem.panel.nativeElement;
panel.addEventListener('scroll', event => this.loadNextOnScrollLang(event, this.selectElem.ngControl.name));
}
}
loadNextOnScrollLang(event, select_name) {
if (this.hasScrolledToBottomLang(event.target)) {
this.viewIndex += this.windowSize;
this.modifyPageDetails(select_name)
this.appService.getAllLanguages(this.standardPageSizeObj, resp => {
console.log('list of languages', resp)
this.fetchLanguage.push(...resp['data'])
}, (error) => {
});
}
}
private hasScrolledToBottomLang(target): boolean {
return Math.abs(target.scrollHeight - target.scrollTop - target.clientHeight) < this.PIXEL_TOLERANCE;
}
The code for currency drop down stays the same. SO, duplication is my first problem. The second this is on scrolling there are two API calls made instead of one. I can live with that but since there are 6 fields, the repition is way too much.
Due to certain security restrictions I cant even use an external library. Is there a better way to implement this. Please let me know if any more clarification is required. Thanks
Here is how you can do it:
STEP 1
For each mat-select
you should create a local variable with ViewChild
.
@ViewChild('select_1', { static: false }) select_1: MatSelect;
@ViewChild('select_2', { static: false }) select_2: MatSelect;
@ViewChild('select_3', { static: false }) select_3: MatSelect;
@ViewChild('select_4', { static: false }) select_4: MatSelect;
STEP 2
Create a function that will be triggered when any of the dropdowns is opened. You can use the (openedChange)
event emitter and check it the value of the event is true
. Second parameter to the function can be the dropdown that is opened, so we don't have repetitive code for each dropdown.
<mat-form-field>
<mat-select placeholder="Select 1" #select_1 (openedChange)="onOpenedChange($event, 'select_1')">
<mat-option *ngFor="let item of select_1_options;">
{{item}}
</mat-option>
</mat-select>
</mat-form-field>
STEP 3
Define the onOpenedChange()
function that will be triggered on openedChange
event. That function should check if the scrollTop
of the dropdown's panel
is equal to (scrollHeight
-offsetHeight
) because that means that the user scrolled to bottom of the scroll. When that happens you can call the API.
onOpenedChange(event: any, select: string) {
if (event) {
this[select].panel.nativeElement.addEventListener('scroll', (event: any) => {
if (this[select].panel.nativeElement.scrollTop === this[select].panel.nativeElement.scrollHeight - this[select].panel.nativeElement.offsetHeight) {
console.log(`Call API for ${select}`);
}
});
}
}
Here is the working code: https://stackblitz.com/edit/angular-ivy-xritb1?file=src%2Fapp%2Fapp.component.ts
We can create a directive that handles this scrolling logic for us and we can prevent code duplication.
here is a demo: https://stackblitz.com/edit/angular-ivy-m2gees?file=src/app/mat-select-bottom-scroll.directive.ts
And Here is a brief explanation:
Create a custom directive and get the reference of MatSelect
inside it.
import {Directive,ElementRef,EventEmitter,Input,OnDestroy,Output} from '@angular/core';
import { MatSelect } from '@angular/material/select/';
import { fromEvent, Subject } from 'rxjs';
import { filter, switchMap, takeUntil, throttleTime } from 'rxjs/operators';
@Directive({
selector: '[appMatSelectScrollBottom]'
})
export class MatSelectScrollBottomDirective implements OnDestroy {
private readonly BOTTOM_SCROLL_OFFSET = 25;
@Output('appMatSelectScrollBottom') reachedBottom = new EventEmitter<void>();
onPanelScrollEvent = event => {};
unsubscribeAll = new Subject<boolean>();
constructor(private matSelect: MatSelect) {
this.matSelect.openedChange
.pipe(filter(isOpened => !!isOpened),
switchMap(isOpened =>fromEvent(this.matSelect.panel.nativeElement, 'scroll').pipe(throttleTime(50))), //controles the thrasold of scroll event
takeUntil(this.unsubscribeAll)
)
.subscribe((event: any) => {
console.log('scroll');
// console.log(event, event.target.scrollTop, event.target.scrollHeight);
if (
event.target.scrollTop >= (event.target.scrollHeight - event.target.offsetHeight - this.BOTTOM_SCROLL_OFFSET)) {
this.reachedBottom.emit();
}
});
}
ngOnDestroy(): void {
this.unsubscribeAll.next(true);
this.unsubscribeAll.complete();
}
}
This directive will emit an event as soon as the scroll reaches to the bottom.
We are starting with the openedChange
event and then we switchMap
ed it to the scroll event of the selection panel. SwitchMap will automatically unsubscribe from the older scroll event as soon the new open event fires.
In your component use this directive to listen to the event as follows.
<mat-form-field>
<mat-select placeholder="Choose a Doctor" (openedChange)="!$event && reset()"
(appMatSelectScrollBottom)="loadAllOnScroll()"><!-- liste to the event and call your load data function -->
<mat-option *ngFor="let dr of viewDoctors;let i = index">{{dr}}</mat-option>
</mat-select>
</mat-form-field>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With