Search code examples
angularangular-materialscrollbar

Angular 7 - Virtual scrollbar resets the selected item in multiselect while scrolling


I have a multi select drop down with virtual scrollbar inside it.

Html

<mat-form-field>
  <mat-label>Toppings</mat-label>
  <mat-select [formControl]="multiSelectControl" multiple [(value)]="selected" (openedChange)="openChange($event)">
    <cdk-virtual-scroll-viewport itemSize="5" minBufferPx="200" maxBufferPx="400" [style.height.px]=5*48>
      <button (click)="selectAll()">Select All</button>
      <button (click)="clear()">Clear</button>
      <mat-option *cdkVirtualFor="let topping of toppingList" [value]="topping">{{topping}}</mat-option>
    </cdk-virtual-scroll-viewport>
  </mat-select>
</mat-form-field>

The .ts file:

export class AppComponent {
  title = 'test-proj';

  toppings = new FormControl();
  toppingList: string[] = ['Extra cheese', 'Mushroom', 'Onion', 'Pepperoni', 'Sausage', 'Tomato'];
  selected: any;

  @ViewChild(CdkVirtualScrollViewport)
  cdkVirtualScrollViewPort: CdkVirtualScrollViewport;

  multiSelectControl = new FormControl();

  constructor() {
    for (let i = 0; i < 4000; i++) {
      this.toppingList.push('gjkgkf--' + i);
    }
  }

  selectAll() {
    this.selected = this.toppingList;
    this.multiSelectControl.patchValue(this.toppingList);
  }

  clear() {
    this.selected = [];
    this.multiSelectControl.patchValue([]);
  }

  openChange($event: boolean) {
    if ($event) {
      this.cdkVirtualScrollViewPort.scrollToIndex(0);
      this.cdkVirtualScrollViewPort.checkViewportSize();
    }
  }

I have added 4000 items in the dropdown. If I select the first 2 items and the last two items and scrolls, then the selection check on the first two items disappears.

Please suggest. Thanks


Solution

  • I've created a sort of workaround for the bug itself. It uses the onSelectionChange of the mat-option element. On top of that, it checks for changes on the options list as well, and selects if necessary. It's raw unoptimized code, but you'll get the idea:

    @ViewChildren(MatOption)
    options: QueryList<MatOption>;
    
    constructor(private cd: ChangeDetectorRef) {}
    
    ngAfterViewInit(): void {
      this.options.changes.subscribe(() => {
        let needUpdate = false;
    
        this.options.forEach((option) => {
          const selected = this.selected.includes(option.value);
           if (selected && !option.selected) {
            option.select();
            needUpdate = true;
          } else if (!selected && option.selected) {
            option.deselect();
            needUpdate = true;
          }
        });
        if (needUpdate) {
          this.cd.detectChanges();
        }
      });
    }
    
    onSelectionChange(change): void {
      if (!change.isUserInput) {
        return;
      }
    
      const value = change.source.value;
      const idx = this.selected.indexOf(change.source.value);
    
      if (idx > -1) {
        this.selected.splice(idx, 1)
      } else {
        this.selected.push(value);
      }
    }
    

    And this is the mat-select:

    <mat-select [formControl]="multiSelectControl" multiple [value]="selected" (openedChange)="openChange($event)">
      <cdk-virtual-scroll-viewport itemSize="5" minBufferPx="200" maxBufferPx="400" [style.height.px]=5*48>
        <button (click)="selectAll()">Select All</button>
        <button (click)="clear()">Clear</button>
        <mat-option *cdkVirtualFor="let topping of toppingList" [value]="topping" (onSelectionChange)="onSelectionChange($event)">{{topping}}</mat-option>
      </cdk-virtual-scroll-viewport>
    </mat-select>
    

    With a working stackblitz


    Be aware though that if you have a dynamic lists which can update its contents, and a selection that has been made is removed from that list, it will still be selected, even though that should not be possible anymore