Search code examples
angularangular2-templatestring-interpolation

Interpolate a dynamic template with ng-select


I want to create a component that contains an ng-select component to which I am passing an array of complex objects and definitions of what fields to display in the dropdown and for the selected value. With ng-select you specify the field to be displayed (bindLabel) and define a template to display the selected value (you could display one or more fields from the object, or other HTML markup). I am able to pass the value for bindLabel, but can't figure out how to interpolate the template.

For instance this is how I'd normally use ng-select. In this case I am displaying two of the object's fields and some HTML (bolding the abbreviation field), and listing the abbreviation fields in the dropdown:

child component

  items = [
    { name: 'United States', abbreviation: 'US' }, 
    { name: 'United Kingdom', abbreviation: 'UK' }, 
    { name: 'Canada', abbreviation: 'CA' }
  ];
  displayField = 'abbreviation';

child template

  <ng-select [items]="items" 
           bindLabel="displayField"
           [(ngModel)]="model"
           name="ngselect" 
           (change)=emitModelChanged()>
      <ng-template ng-label-tmp let-item="item">
          <b>{{item.abbreviation}}</b> - {{item.name}}
      </ng-template>
  </ng-select>

To configure it dynamically from a parent component I pass items, displayField and template as Inputs:

parent component

  selectedTemplate = '<b>{{item.name}}</b> - {{item.abbreviation}}';

parent template

  <child-component [model]=model 
                   [items]=items
                   [displayField]="'abbreviation'"
                   [template]=selectedTemplate
                   (update)=updateModel($event)></child-component>

child component

  @Input() items;
  @Input() displayField; //what field shows in dropdown options
  @Input() template; // what shows for selected value in combobox
  @Input() model;
  @Output() update: EventEmitter<any> = new EventEmitter(); //emit back to parent

child-component template

  <ng-select [items]="items" 
           bindLabel="{{displayField}}"
           [(ngModel)]="model"
           name="ngselect" 
           (change)=modelChanged()>
      <ng-template let-item="item">
          <label [innerHTML]="template"></label>
      </ng-template>
</ng-select>

While the bold tag of "template" is interpreted, the data fields are not interpolated, the value displays literally as

{{item.name}} - {{item.abbreviation}}

Is it losing scope and thus not interpolating {{item.name}} to the appropriate value? The same happens when instead of the label with innerHTML I just use {{template}}. How can I prevent this from being rendered as a string?

I likewise have the same interpolation fail with a standard <select>, it renders the options as literal strings:

selectField = 'item.'+this.displayField;   // (equivalent to item.abbreviation)

<select #standardSelect [(ngModel)]="model" (change)=modelChanged() >

  <!-- Also getting interpolating error here. Below renders as a string "item.abbreviation" -->
  <option *ngFor="let item of items" [ngValue]="item">{{selectField}}</option>

  <!-- This also renders as a string -->
  <!-- <option *ngFor="let item of items" [ngValue]="item" [innerHTML]="selectField"></option> -->

  <!-- Hardcoded value below works -->
  <!-- <option *ngFor="let item of items" [ngValue]="item">{{item.abbreviation}}</option> -->
</select>

Stackblitz


Solution

  • you can path template into your component and render it with ng-container, but you need use ContentChild to handle template reference. for several templates use # naming to match them with ContentChild references. you can try as follow example

    use TemplateRef to reference outer template

        import { TemplateRef } from '@angular/core';
        
        @Component({
          selector: 'ng-select-accessible[displayField]',
          templateUrl: './ng-select-accessible.component.html',
          styleUrls: [ './ng-select-accessible.component.scss' ]
        })
        export class NgSelectAccessibleComponent  {
        
          @ContentChild('labelTemplate') labelTemplate: TemplateRef<any>;;
          @ContentChild('optionTemplate') optionTemplate: TemplateRef<any>;;
    
        }
    

    use ng-container to place outer template inside of item template

        <div class='styled-select' aria-hidden=true>
          <ng-select [items]="items"
                     [placeholder]=placeholder 
                     [(ngModel)]="model"
                     name="ngselect" 
                     (change)=modelChanged()
                     attr.aria-label={{ariaLabel}}>
    
              <ng-template ng-label-tmp let-item="item">
                <ng-container 
                  *ngTemplateOutlet="labelTemplate; context: { $implicit: item }">
                </ng-container>
              </ng-template>
        
              <ng-template ng-option-tmp let-item="item">
                <ng-container 
                  *ngTemplateOutlet="optionTemplate; context: { $implicit: item }">
                </ng-container>
              </ng-template>
          </ng-select>
        </div>
    

    define item view in ng-template as follow

        <ng-select-accessible 
                          [model]=model 
                          [items]=items
                          [displayField]="'abbreviation'"
                          [placeholder]="'custom placeholder'" 
                          (update)=updateModel($event)
                          [ariaLabel]="'Select a number'">
          <ng-template #labelTemplate let-item>               
            <label>
              <b>{{item.name}}</b> - {{item.abbreviation}}
            </label>
          </ng-template>
          <ng-template #optionTemplate let-item>               
            <label>
              <b>{{item.name}}</b>
            </label>
          </ng-template>
    
        </ng-select-accessible>
    

    i think this one is a good article about ng-template