尝试在叠加层中使用 mat-select-list 制作自定义多 select 搜索组件

Trying to make a custom multi select search component with mat-select-list in overlay

我正在尝试制作自定义 Angular Material multiSelect 可过滤组件,例如这个:

我用这个代码做了一个多select-search.component:

export interface MultiSelectSearchOption {
    label: string;
    value: any;
}

export interface MultiSelectOverlayData {
    options: MultiSelectSearchOption[];
}

export const MULTI_SELECT_OVERLAY_DATA = new InjectionToken<
    MultiSelectOverlayData
>('MULTI_SELECT_OVERLAY_DATA');



//TEXT INPUT COMPONENTS
@Component({
    selector: 'multiSelectSearch',
    templateUrl: 'multiSelectSearch.component.html',
    styleUrls: ['./multiSelectSearch.component.scss'],
})
export class MultiSelectSearchComponent implements AfterViewInit {
    @Input() list: any[] = [];
    @Input() selection: any;
    /**
     * @param filterKey
     * @description the key of the object to filter out with the text input
     */
    @Input() filterKey: string;
    /**
     * @param labelKey
     * @description the key of the object to be used as label of option
     */
    @Input() labelKey: string;
    @Input() placeholder: string;
    @Output() valueChange = new EventEmitter<any[]>();
    @ViewChild('input', { static: false }) inputViewRef: ElementRef;
    public search = new FormControl('');
    private listOptions: MultiSelectSearchOption[] = [];
    private overlayRef: OverlayRef;
    constructor(private overlay: Overlay) {}

    ngAfterViewInit() {
        if (!this.list.length || !this.labelKey || !this.filterKey) {
            console.error(
                'Component usage require input of list, labelKey, filterKey component'
            );
            throw new Error();
        } else {
            this.search.valueChanges
                .pipe(debounceTime(1000))
                .subscribe(search => {
                    this.listOptions = (!search.length
                        ? this.list
                        : this.list.filter(e =>
                              e[this.filterKey]
                                  .toString()
                                  .toUpperCase()
                                  .startsWith(search.toUpperCase())
                          )
                    ).map(
                        (e: any): MultiSelectSearchOption => ({
                            label: e[this.labelKey],
                            value: e,
                        })
                    );
                    const tokens = new WeakMap();
                    tokens.set(MULTI_SELECT_OVERLAY_DATA, {
                        options: this.listOptions,
                    });
                    this.overlayRef = this.overlay.create({
                        hasBackdrop: false,
                        minWidth: '10vw',
                        minHeight: '10vh',
                        positionStrategy: this.overlay
                            .position()
                            .flexibleConnectedTo(this.inputViewRef)
                            .withPositions([
                                {
                                    offsetX: 0,
                                    offsetY: 0,
                                    originX: 'start',
                                    originY: 'top',
                                    overlayX: 'start',
                                    overlayY: 'top',
                                    panelClass: [],
                                    weight: 1,
                                },
                            ]),
                    });
                    const multiSelectPortal = new ComponentPortal(
                        MultiSelectOverlayComponent,
                        null,
                        tokens
                    );
                    this.overlayRef.attach(multiSelectPortal);
                });
        }
    }
}

// OVERLAY COMPONENT
@Component({
    selector: 'multi-select-overlay',
    template: `
        <mat-card>
            <mat-selection-list #list>
                <mat-list-option
                    *ngFor="let option of listOptions"
                    [value]="option.value"
                    >{{ option.label }}</mat-list-option
                >
            </mat-selection-list>
        </mat-card>
    `,
})
export class MultiSelectOverlayComponent implements AfterViewInit {
    @ViewChild('list', { static: false }) list: MatSelectionList;
    public get listOptions() {
        return this.data.options as MultiSelectSearchOption[];
    }
    constructor(
        @Inject(MULTI_SELECT_OVERLAY_DATA)
        private data: MultiSelectOverlayData
    ) {
        console.log('data', data);
    }

    ngAfterViewInit() {
        this.list.selectionChange.pipe(
            //emit value
            tap(x => console.log(x))
        );
    }
}

一切似乎都运行良好,但是当我尝试遍历我的 data.options 元素时,出现以下错误:

我不明白为什么 ComponentPortal 创建的组件无法在 Array 上使用 ngFor?

再生产

使用 StackBlitz 来演示您正在尝试做什么: 完整的错误和代码在这里: https://components-issue-55qrra.stackblitz.io/

环境

我把 data 对象的 console.log 传过去了,我可以看到它确实是一个数组:

前往 stackblitz https://components-issue-55qrra.stackblitz.io/

收到错误消息(在 Stackblitz 上看到): ERROR Error: Cannot find a differ supporting object '[object Object]' of type 'object'. NgFor only supports binding to Iterables such as Arrays.

从 angular material 支持得到这个答案:

The root cause of the issue is that the overlay component is being given an injector that does not contain differs for ngFor. This is because you created the overlay as a component without any parent injector that had these differs. In multi-select-search.component.ts, when you create the ComponentPortal, be sure to include the current injector using a PortalInjector:

import { PortalInjector } from '@angular/cdk/portal';
...
const tokens = new WeakMap();
tokens.set(MULTI_SELECT_OVERLAY_DATA, {
  options: this.listOptions,
});
const injector = new PortalInjector(this.injector, tokens);

...

const multiSelectPortal = new ComponentPortal(MultiSelectOverlayComponent, null, injector);

希望这可以帮助到别人!