Search code examples
angularangular-materialangular15mat-selectangular-cdk-virtual-scroll

cdk-virtual-scroll-viewport with search doesn't display the value


It has been days that I am trying to fix some angular 15 code but I can't find the solution. I am using a mat-select(Legacy) with ngx-mat-select-search and virtual scroll cdk-virtual-scroll-viewport. When I select a value, it is displayed, but when I search for some value, it's not displayed but it's read, I can display it in my console but not in my mat-select.

I am facing the same problem in this stackblitz link, you can search for "Bank C" and select it, it's displayed, but when you search for "Bank Q" and select it, the value is not displayed.

Here's my html code :

<mat-form-field>
<mat-select [formControl]="formControl"
            [placeholder]="placeholder"
            [(ngModel)]="values"
            [matTooltip]="getTooltip(values)"
            [multiple]="multiple"
            [required]="required"
            (openedChange)="openChange($event)">

    <mat-option *ngIf="!hideSearch">
        <ngx-mat-select-search
            [placeholderLabel]="filterItem"
            [noEntriesFoundLabel]="noItemMatch"
            [formControl]="filterItemCtrl"
            [showToggleAllCheckbox]="showAllCheckbox"
            [toggleAllCheckboxChecked]="showAllCheckbox && multiple && items.length && values.length === items.length"
            (toggleAll)="checkAllItems($event)">
        </ngx-mat-select-search>
    </mat-option>

    <mat-option *ngIf="emptyItem">{{ emptyItem }}</mat-option>

    <cdk-virtual-scroll-viewport [itemSize]="5" [style.height.px]=viewPortHeightPx class="custom-viewport">
        <mat-option *cdkVirtualFor="let item of filteredItems | async;" 
                    [value]="item"
                    (onSelectionChange)="clickOption($event)">
            <div >
                {{ item }}
            </div>
         </mat-option>
    </cdk-virtual-scroll-viewport>
</mat-select>

Thanks for helping me.


Solution

  • This one was really tough to debug, but basically the problem was that when an mat-option is outside the rendering window of the virtual scroll the option will not get selected, so I wrote a special slice method, that will always place the selected bank on the top of the array, so that will get surely selected and that fixed your issue.

      getCorrectSlice(): Bank[] {
        const output = this.banks.slice();
        const bank: Bank = this.bankCtrl.value;
        if (bank) {
          return [bank, ...this.banks];
        }
        return this.banks.slice();
      }
    

    Above code creates the slice with the selected value in the beginning!

    html

    <h3>Single selection</h3>
    <p>
      <mat-form-field>
        <mat-select
          id="bank"
          id="bank"
          [formControl]="bankCtrl"
          placeholder="Bank"
          #singleSelect
          [compareWith]="compareWith"
          (selectionChange)="selectionChange($event)"
        >
          <mat-option>
            <ngx-mat-select-search
              [formControl]="bankFilterCtrl"
            ></ngx-mat-select-search>
          </mat-option>
    
          <!-- <mat-option *ngFor="let bank of filteredBanks | async" [value]="bank">
            {{bank.name}}
          </mat-option> -->
          <cdk-virtual-scroll-viewport [itemSize]="42" [style.height.px]="4 * 42">
            <mat-option [value]="{ name: 'any', id: -1 }">Any</mat-option>
            <mat-option
              *cdkVirtualFor="let bank of filteredBanks | async; trackBy: trackBy"
              [value]="bank"
              >{{ bank.name }}</mat-option
            >
          </cdk-virtual-scroll-viewport>
        </mat-select>
      </mat-form-field>
    </p>
    <p>Selected Bank: {{ bankCtrl.value?.name }}</p>
    

    ts

    import {
      AfterViewInit,
      Component,
      OnDestroy,
      OnInit,
      ViewChild,
    } from '@angular/core';
    import { FormControl } from '@angular/forms';
    import { MatSelect } from '@angular/material/select';
    import { ReplaySubject, Subject } from 'rxjs';
    import { take, takeUntil } from 'rxjs/operators';
    
    import { Bank, BANKS } from '../demo-data';
    
    @Component({
      selector: 'app-single-selection-example',
      templateUrl: './single-selection-example.component.html',
      styleUrls: ['./single-selection-example.component.scss'],
    })
    export class SingleSelectionExampleComponent
      implements OnInit, AfterViewInit, OnDestroy
    {
      /** list of banks */
      protected banks: Bank[] = BANKS;
    
      /** control for the selected bank */
      public bankCtrl: FormControl = new FormControl();
    
      /** control for the MatSelect filter keyword */
      public bankFilterCtrl: FormControl = new FormControl();
    
      /** list of banks filtered by search keyword */
      public filteredBanks: ReplaySubject<Bank[]> = new ReplaySubject<Bank[]>(1);
    
      @ViewChild('singleSelect', { static: true }) singleSelect: MatSelect;
    
      /** Subject that emits when the component has been destroyed. */
      protected _onDestroy = new Subject<void>();
    
      constructor() {}
    
      ngOnInit() {
        // set initial selection
    
        // load the initial bank list
        this.bankCtrl.setValue(this.banks[10]);
        this.filteredBanks.next(this.getCorrectSlice());
    
        // listen for search field value changes
        this.bankFilterCtrl.valueChanges
          .pipe(takeUntil(this._onDestroy))
          .subscribe(() => {
            this.filterBanks();
          });
      }
    
      ngAfterViewInit() {}
    
      ngOnDestroy() {
        this._onDestroy.next();
        this._onDestroy.complete();
      }
    
      getCorrectSlice(): Bank[] {
        const output = this.banks.slice();
        const bank: Bank = this.bankCtrl.value;
        if (bank) {
          return [bank, ...this.banks];
        }
        return this.banks.slice();
      }
    
      compareWith(a: Bank, b: Bank) {
        return a && a.name && b && b.name ? a === b : false;
      }
    
      protected filterBanks() {
        let search = this.bankFilterCtrl.value;
        if (!this.banks || !search) {
          return;
        }
        // get the search keyword
        if (!search) {
          this.filteredBanks.next(this.getCorrectSlice());
          return;
        } else {
          search = search.toLowerCase();
        }
        // filter the banks
        this.filteredBanks.next(
          this.banks.filter((bank) => bank.name.toLowerCase().indexOf(search) > -1)
        );
      }
    
      selectionChange(e: any) {
        this.bankFilterCtrl.setValue('');
        this.filteredBanks.next(this.getCorrectSlice());
      }
    
      trackBy(index: number, item: Bank) {
        return item.id;
      }
    }
    

    stackblitz