7

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

NeNaD
  • 18,172
  • 8
  • 47
  • 89
MenimE
  • 274
  • 6
  • 18

2 Answers2

2

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>
HirenParekh
  • 3,655
  • 3
  • 24
  • 36
  • Hey Hiren, thanks for the detailed answer. I will try and implement this and let you know if this works for me. Cheers – MenimE Sep 01 '21 at 07:44
  • 1
    @MenimE I am glad that you noticed my answer. However, it is sad that the bounty on this question is expired before you check any answer. Just let me know if you find my answer helpful. – HirenParekh Sep 02 '21 at 15:36
2

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

NeNaD
  • 18,172
  • 8
  • 47
  • 89
  • 1
    Hey! Some of the screens I work on have 30+ drop downs, creating a viewChild for each will be a very tedious task, i guess. – MenimE Sep 01 '21 at 07:45