I have a custom select component and want to use ng-content
to pass my options into it, like this:
<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>
This doesn't seem to work though. It didn't even display the options at first. I found a hack to get them to display, but I still can't select anything. Here's my component:
<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>
Is there any way to make this work or does mat-select
simply not work with ng-content
?
I know that I could use @Input()
to pass the options into the component but I think the code looks a lot cleaner when using ng-content
.
EDIT: It seems like I actually can select items. The problem is that I can select multiple options and there is a ripple effect, even though disableRipple
is existent on my mat-select
.
there're a work-around. put the ng-content in a div hidden and create the options asking about ContentChildren(MatOption), see the example in stackblitz
The component is
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;
});
}
}
The use
<custom-select>
<mat-option *ngFor="let food of foods" [value]="food.value">
{{food.viewValue}}
</mat-option>
</custom-select>
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With