Angular Use same TemplateRef in more than one place at a time - Clone TemplateRef?

Viewed 674

I'm trying to build a multi-select component which can take a template for each select option element. It's mostly working, the templates are grabbed from the ng-content in the option component and displays in the "select popover" fine.

Options display fine

I'm having issues when trying to display the selected option in the select component. It seems that the template can only be shown in one place at a time, so when the selected option is displayed in the select, it is removed from the popover.

Options display wrong

Is there any way I could clone the TemplateRef component? I don't actually mind if the context doesn't update for the template as I'm not currently using anything from it, just the template.

Including some code snips below for clarity, I think it's okay though.

Usage in somepage.component.html:

<my-select [(value)]="vals">
    <my-option *ngFor="let x of [1,2,3]" [value]="x">Option {{x}}</my-option>
</my-select>`

my-select.component.html

...
<ng-container [ngTemplateOutlet]="getSelectedOptionTemplate()"></ng-container>
...

my-select.component.ts

getSelectedOptionTemplate(){
    // Some way to clone here could solve the issue?
    return this.getSelectedOption().template;
}

my-select-overlay.component.html

...
<ng-container *ngFor="let opt of options;">
    <ng-container [ngTemplateOutlet]="getTemplate(opt)"></ng-container>
</ng-container>
...

my-option.component.html

<ng-template>
    <ng-content></ng-content>
</ng-template>

my-option.component.ts

...
@ViewChild(TemplateRef)
template: TemplateRef<any>;
...
1 Answers

I've found on Github a solution for this issue.

It seems that using the same TemplateRef works when the ng-template is wrapping the component that contains the ng-content.

Imagine that we need to obtain something like:

<ng-template>
   <my-option>
      ... option content ...(<ng-content></ng-content>)
   </my-option>
</ng-template>`

In order to obtain the structure above, we can implement a structural directive. From the docs:

Angular transforms the asterisk in front of a structural directive into an <ng-template> that surrounds the host element and its descendants.

The code for the structural directive would be:

@Directive({
  selector: "[appOptionTemplate]"
})
export class OptionTemplateDirective {
  @Input("appOptionTemplate")
  value: any;

  constructor(
    public templateRef: TemplateRef<any>
  ) {}
}

(note the following:

  • the injection of the templateRef in the constructor
  • the Input property)

When populating the select options, we'll use the following:

<app-select>
  <ng-container *ngFor="let x of [1,2,3]; let i=index;">
    <app-option *appOptionTemplate="x">
      Option {{x}}
    </app-option>
  </ng-container>
</app-select>

Now, we'll use the structural directive for querying the content in the select component:

@ContentChildren(OptionTemplateDirective)
options: QueryList<OptionTemplateDirective>;

And for displaying the option, we'll use:

<ng-container *ngTemplateOutlet="option.templateRef"></ng-container>

Example: https://stackblitz.com/edit/angular-ivy-pues8t


Github topic:

https://github.com/angular/angular/issues/37995


More info about structural directives:

https://angular.io/guide/structural-directives

Related