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
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