Adding to the answer of @Eliseo and answering the question of @SadHippo123 regarding the empty viewValue:
You do not have to use the yet property to *ngIf the mat-option, since @ContentChildren checks only the children inside ng-content.
To solve the issue with the empty viewValue, instead of assigning the values in the afterviewinit hook, you can subscribe to the QueryList.changes observable, and assign it there, solved the problem for me:
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();
}
I also created a custom mat-select component, with additional features. Here is my version if anyone is interested:
CustomSelectDropdown.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);
}
}
CustomSelectDropdown.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>
If you copy paste the code and it looks weird, that is because i also styled the mat-options/mat-select in a seperate file. This sample is simply for functionality.