網站搜尋

angular - 是否可以使用 ng-content 將 mat-options 傳遞給我自訂的 mat-select 元件?


我有一個自訂的選取元件,想要使用 ng-content 將我的選項傳入其中,像這樣:

<lib-select [(selected)]="selected" (selectedChange)="onChange($event)">
            <mat-option [value]="0">Value 1</mat-option>
            <mat-option [value]="1">Value 2</mat-option>
            <mat-option [value]="2">Value 3</mat-option>
            <mat-option [value]="3">Value 4</mat-option>
            <mat-option [value]="4">Value 5</mat-option>
</lib-select>

不過這似乎不起作用。起初甚至沒有顯示選項。我找到了一個讓它們顯示的破解方法,但我仍然無法選擇任何東西。這是我的組件:

    <mat-select panelClass="select" disableRipple (selectionChange)="onChange()" [(value)]="selected" disableOptionCentering>
        <mat-select-trigger>{{selected}}</mat-select-trigger>
        <!-- mat-option below is required to render ng-content in mat-select. this is an ugly hack and there might be a better workaround for this -->
        <mat-option [value]="" style="display: none;"></mat-option>
        <ng-content></ng-content>
    </mat-select>

有沒有任何方法可以讓這個工作,還是說 mat-select 根本無法與 ng-content 一起使用?

我知道我可以使用 @Input() 將選項傳入元件,但我覺得使用 ng-content 時代碼看起來會乾淨許多。

編輯:看起來我其實可以選取項目。問題是我可以選取多個選項,並且會有波紋效果,儘管我的 mat-select 上已經存在 disableRipple。

答案#1

有一個變通方法。將 ng-content 放在一個隱藏的 div 中,然後創建詢問 ContentChildren(MatOption) 的選項,參見 stackblitz 中的範例

該元件是

import {Component, ContentChildren, AfterViewInit, QueryList} from "@angular/core";
import { MatOption } from "@angular/material/core";

@Component({
  selector: "custom-select",
  template: `
    <mat-form-field>
      <mat-label>Favorite food</mat-label>
      <mat-select>
        <ng-container *ngIf="yet">
          <mat-option *ngFor="let option of options" [value]="option.value">
            {{ option.viewValue }}
          </mat-option>
        </ng-container>
      </mat-select>
    </mat-form-field>
    <div style="display:none" *ngIf="!yet">
      <ng-content></ng-content>
    </div>
  `
})
export class CustomSelect implements AfterViewInit {
  @ContentChildren(MatOption) queryOptions: QueryList<MatOption>;
  options: any[];
  yet: boolean;
  ngAfterViewInit() {
    this.options = this.queryOptions.map(x => {
      return { value: x.value, viewValue: x.viewValue };
    });
    setTimeout(() => {
      this.yet = true;
    });
  }
}

使用

<custom-select>
    <mat-option *ngFor="let food of foods" [value]="food.value">
      {{food.viewValue}}
    </mat-option>
</custom-select>

答案#2

補充 @Eliseo 的回答並回應 @SadHippo123 關於空的 viewValue 的問題:

你不需要使用 yet 屬性來 *ngIf mat-option,因為 @ContentChildren 僅檢查 ng-content 裡的子元素。

為了解決 empty viewValue 的問題,不要在 afterviewinit 鉤子中賦值,而是可以訂閱 QueryList.changes 可觀察對象,然後在那裡賦值,這解決了我的問題:

ngAfterViewInit(): void {
    this.queryOptions.changes.pipe(
      takeUntil(this.destroy$),
      tap((changes: MatOption[]) => {
        this.transformedOptions = changes.map((option) => {
          return { value: option.value, viewValue: option.viewValue }
        });
      })
    ).subscribe();
  }

我也創建了一個自訂的 mat-select 元件,具有額外的功能。如果有人有興趣,這是我的版本:

自訂選擇下拉選單.ts

import { AfterViewInit, Component, ContentChildren, DoCheck, EventEmitter, Input, IterableDiffer, IterableDiffers, OnChanges, OnDestroy, OnInit, Output, QueryList, SimpleChanges } from '@angular/core';
import { AbstractControl, FormControl } from '@angular/forms';
import { MatOption } from '@angular/material/core';
import { MatSelectChange } from '@angular/material/select';
import { Subject } from 'rxjs';
import { takeUntil, tap } from 'rxjs/operators';
import { hasChanged } from '../../../helpers/has-changed';

@Component({
  selector: 'app-custom-select-dropdown',
  templateUrl: './custom-select-dropdown.component.html',
  styleUrls: ['./custom-select-dropdown.component.scss']
})
export class CustomSelectDropdownComponent implements OnInit, AfterViewInit, OnChanges, DoCheck, OnDestroy {

  /**
   * In case you want to use custom mat-options, you can insert them via ng-content, in that case do not provide 
   * options, control, value-key-name, view-value-key-name or translate-view-value properties
   * Example: 
   * <app-custom-select-dropdown [label]="label" [selected]="selected">
   *     <mat-option *ngFor="myOption of MyOptions" [value]="myOption.custom.property"> 
   *         {{myOption.custom.price | currency}}
   *     </mat-option>
   * </app-custom-selec-dropdown>
   */
  @ContentChildren(MatOption) queryOptions: QueryList<MatOption>;

  @Input('options') options: any[]
  @Input('control') control: AbstractControl = new FormControl(); // bind the mat-select to a formControl, do not use with selected
  @Input() selected: any; // sets initially selected value, can be used for two way property binding, only use when control or ng-content are not used
  @Input('value-key-name') valueKeyName: string; // if option value should be option property, provide key name for it
  @Input('view-value-key-name') viewValueKeyName: string; // if option view value should be option property, provide key name for it
  @Input('translate-view-value') translateViewValue: boolean = false; // set to true if view value should be translated;
  @Input('label') label: string;
  @Input('disabled') disabled: boolean;

  @Output() selectedChange = new EventEmitter<any>();

  destroy$ = new Subject();
  transformedOptions: { value: any, viewValue: string }[] = [];
  iterableDiffer: IterableDiffer<any>;

  constructor(private _iterableDiffers: IterableDiffers) {
    this.iterableDiffer = _iterableDiffers.find([]).create(null);
  }

  ngOnInit(): void { }

  ngAfterViewInit(): void {
    this.queryOptions.changes.pipe(
      takeUntil(this.destroy$),
      tap((changes: MatOption[]) => {
        this.transformedOptions = changes.map((option) => { return { value: option.value, viewValue: option.viewValue } });
      })
    ).subscribe();
  }

  ngOnChanges(changes: SimpleChanges): void {
    if (hasChanged(changes, 'selected')) {
      this.control.setValue(this.selected);
    }
  }

  ngDoCheck(): void {
    // using iterable differ and doCheck to detect changes in arrays
    let changes = this.iterableDiffer.diff(this.options);
    if (changes) {
      this.transformedOptions = this.transformOptions(this.options);
    }
  }

  ngOnDestroy(): void {
    this.destroy$.next();
  }

  transformOptions(options: any[]): any[] {
    return options.map(option => {
      return {
        value: this.valueKeyName ? option[this.valueKeyName] : option,
        viewValue: this.viewValueKeyName ? option[this.viewValueKeyName] : option
      }
    })
  }

  selectionChange($event: MatSelectChange) {
    this.selectedChange.emit($event.value);
  }
}

自訂選擇下拉選單.html

<div class="dropdown-wrapper">
    <span [ngClass]="{'disabled': disabled}">{{label}}:</span>
    <mat-form-field appearance="fill" floatLabel="always" class="filter-drop-down">
        <mat-select [formControl]="control" [(value)]="selected" [disabled]="disabled"
            (selectionChange)="selectionChange($event)" panelClass="custom-mat-select" disableOptionCentering="true">
            <mat-option *ngFor="let option of transformedOptions" [value]="option.value">
                {{ !translateViewValue ? option.viewValue : option.viewValue | translate}}
            </mat-option>
        </mat-select>
        <button mat-icon-button matSuffix [disabled]="disabled">
            <mat-icon svgIcon="material:expand_more"></mat-icon>
        </button>
    </mat-form-field>
</div>

<div style="display: none">
    <ng-content></ng-content>
</div>

如果你複製粘貼程式碼後看起來很奇怪,那是因為我也在另一個檔案中對 mat-options/mat-select 進行了樣式設計。這個範例僅用於功能演示。