Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Trigger an event on scrolling to the end of Mat-0ptions in Mat-Select : Angular 6

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

like image 693
MenimE Avatar asked Aug 17 '21 07:08

MenimE


2 Answers

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

like image 76
NeNaD Avatar answered Oct 19 '22 21:10

NeNaD


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 switchMaped 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>
like image 2
HirenParekh Avatar answered Oct 19 '22 22:10

HirenParekh