Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to pass mat-options to my custom mat-select component with ng-content?

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.

like image 715
SadHippo123 Avatar asked Oct 18 '25 11:10

SadHippo123


2 Answers

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>
like image 86
Eliseo Avatar answered Oct 20 '25 01:10

Eliseo


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.

like image 39
Tombalabomba Avatar answered Oct 19 '25 23:10

Tombalabomba