helderabreu
helderabreu

Reputation: 11

Angular Material multi select component with virtual-scroll

I'm trying to create a custom multi select component, based on the angular-material select component (v10), with virtual-scroll and a custom search input inside the component's overlay. I also added a mat-select-trigger element to display the number of selected items, instead of the default list of selected items.

The (almost fully working and without styling) example is available in this stackblitz.

The issue I'm having is related with the displayed value of the selected items.

To replicate the issue you have to:

  1. Search in the list of items (e.g.: 55) and select an option (e.g. item-55)
  2. Click outside of the overlay, to close it
  3. As expected, the component will display 1 selected and the item will be listed in the Selected items list below
  4. Re-open the list, click in the search field clear button and then click outside the overlay, to close it again

The component will now display the Items placeholder, when it should be displaying 1 selected, since the item is still in the Selected items list (and it's this list that it's used in the mat-select-trigger element).

Nonetheless, if you re-open the list and scroll to the selected item (item-55), it will be selected. If you then click outside the overlay to close it again, the correct value will be displayed (1 selected).

I understand it's a virtual-scroll related issue, but I've been unable to find a solution or workaround for this.

Any suggestions would be greatly appreciated!

Upvotes: 1

Views: 612

Answers (1)

George Hulpoi
George Hulpoi

Reputation: 667

For those who still have this problem, I found a workaround for this issue.

Github Issue: https://github.com/angular/components/issues/30559

import {
    Component,
    viewChildren,
    Signal,
    computed,
    signal,
    viewChild,
} from '@angular/core';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatOption, MatSelectModule } from '@angular/material/select';
import {
    CdkVirtualScrollViewport,
    ScrollingModule,
} from '@angular/cdk/scrolling';
import { FormsModule } from '@angular/forms';

@Component({
    selector: 'app-working',
    template: `
        <mat-form-field>
            <mat-label>Working</mat-label>
            <mat-select
                multiple
                [(ngModel)]="selected"
                (openedChange)="handleOpenedChange($event)"
            >
                <cdk-virtual-scroll-viewport
                    itemSize="48"
                    minBufferPx="240"
                    maxBufferPx="240"
                    [style.height.px]="240"
                >
                    @for (option of hiddenOptions(); track option) {
                        <!-- IMPORTANT! Keep the content of 'mat-option' the same -->
                        <mat-option [value]="option" [style.display]="'none'">
                            Option {{ option }}
                        </mat-option>
                    }
                    <mat-option
                        #virtualOption
                        *cdkVirtualFor="let option of options"
                        [value]="option"
                    >
                        Option {{ option }}
                    </mat-option>
                </cdk-virtual-scroll-viewport>
            </mat-select>
        </mat-form-field>
    `,
    standalone: true,
    imports: [
        MatFormFieldModule,
        MatSelectModule,
        ScrollingModule,
        FormsModule,
    ],
})
export class WorkingComponent {
    virtualOptions = viewChildren<MatOption>('virtualOption');
    virtualScroll = viewChild(CdkVirtualScrollViewport);
    selected = signal<number[]>([]);
    options = [...Array(100)].map((value, index) => index + 1);
    hiddenOptions: Signal<number[]>;

    constructor() {
        this.hiddenOptions = computed(() => {
            // The Set data structure allows for O(1) lookup time
            const virtualOptions = new Set(
                this.virtualOptions().map((matOption) => matOption.value),
            );
            return this.selected().filter(
                (value) => !virtualOptions.has(value),
            );
        });
    }

    handleOpenedChange(isOpen: boolean): void {
        const virtualScroll = this.virtualScroll();

        if (!isOpen && virtualScroll) {
            virtualScroll.scrollToOffset(0);
            virtualScroll.checkViewportSize();
        }
    }
}

Read more about it: https://en.george-hulpoi.dev/blog/how-to-make-angular-material-select-work-with-virtual-scroll

Upvotes: 0

Related Questions