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 進行了樣式設計。這個範例僅用於功能演示。
