question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. ItĀ collects links to all the places you might be looking at while hunting down a tough bug.

And, if youā€™re still stuck at the end, weā€™re happy to hop on a call to see how we can help out.

[feature] Ability to clone a TemplateRef to multiple outlets at once

See original GitHub issue

šŸš€ Ability to clone a TemplateRef to multiple outlets at once

Relevant Package

I could see this being a useful utility in @angular/cdk, but would likely require an update to @angular/core. Iā€™m honestly not sure.

Description

Essentially, I would like to have the ability to take a TemplateRef and inject copies of it into multiple template outlets. The use case that made me aware of this limitation was trying to create a <select>-style form control, where consumers could put any arbitrary content into an option (not just text, but icons, images, components, etc.). When an option is selected, the Select component would re-project that optionā€™s content into the Selectā€™s template to indicate the currently selected option.

In case thatā€™s not clear, hereā€™s a simplified/truncated version of the solution that I attempted.

@Component({
  selector: 'my-select',
  template: `
    <div class="selected-value">
      <ng-container *ngTemplateOutlet="selectedTemplate"></ng-container>
      <ng-container *ngIf="!selectedTemplate">{{ placeholder }}</ng-container>
    </div>
    <div class="options">
      <ng-content></ng-content>
    </div>
  `
})
export class SelectComponent implements AfterContentInit {

  @Input() placeholder = "Select an option";
  @ContentChildren(OptionComponent) options: QueryList<OptionComponent>;
  selectedTemplate: TemplateRef<null>;

  ngAfterContentInit() {
    this.options.forEach(option => {
      option.selected.subscribe(() => {
        this.selectedTemplate = option.contentTemplate;
      });
    });
  }

}

@Component({
  selector: 'my-option',
  template: `
    <ng-container *ngTemplateOutlet="contentTemplate"></ng-container>
    <ng-template #contentTemplate>
      <ng-content></ng-content>
    </ng-template>
  `
})
export class OptionComponent {

  @Output() selected = new EventEmitter<void>();
  @ViewChild('contentTemplate') contentTemplate: TemplateRef<null>;

  @HostListener('click') onClick() {
    this.selected.emit();
  }

}

And hereā€™s an example usage that takes advantage of the template cloning:

<label for="status">Task Status</label>
<my-select id="status" [(ngModel)]="statusValue">
  <my-option [value]="Status.Blocked">
    <my-icon icon="ban"></my-icon> Blocked
  </my-option>
  <my-option [value]="Status.Researching">
    <my-icon icon="science"></my-icon> Researching
  </my-option>
  <my-option [value]="Status.InProgress">
    <my-icon icon="ellipsis"></my-icon> In Progress
  <my-option>
  <my-option [value]="Status.Done">
    <my-icon icon="check"></my-icon> Done
  </my-option>
</my-select>

This works as intended, except it appears that the TemplateRef can only be embedded into one view container at any given time. So, when an option is selected, it appears in the SelectComponentā€™s template outlet (šŸŽ‰), but disappears from the OptionComponentā€™s template outlet (šŸ˜­), leaving a blank spot in the options list.

TemplateRef copy attempt

Describe the solution youā€™d like

It would be great if the sample code above just worked as expected out of the box, but I can understand if thatā€™s not feasible, or if itā€™s undesirable for some reason Iā€™m not aware of.

I dug through some of CDKā€™s source code, because I knew that its Drag-and-Drop feature was doing some sort of element cloning for the preview and placeholder elements, so I hoped that maybe they were making use of some Angular template/view APIs that I was unfamiliar with. Unfortunately, it looks like that feature is just using some raw, low-level DOM manipulation to achieve this, which would be way overkill for my use case.

I wonder if it would be possible to add a clone method to the TemplateRef class, which would return a new TemplateRef that could be embedded into an additional template outlet without detaching the original?

Describe alternatives youā€™ve considered

If I specifically wanted to enable icons in my OptionComponent, of course I could add an @Input() icon: IconName, and replicate that to my SelectComponent. I could make the OptionComponentā€™s label a simple @Input label: string while Iā€™m at it, instead of fiddling with the content projection at all. But I like to keep these types of core UI components as flexible as possible for a wide variety of use cases, so that when a new use case comes up that I hadnā€™t originally anticipated, it can be accommodated without needing to make major updates to the component or endlessly tack on configuration options. If thereā€™s another alternative or workaround that I havenā€™t thought of, I would love to hear it.

Issue Analytics

  • State:open
  • Created 3 years ago
  • Reactions:15
  • Comments:12 (5 by maintainers)

github_iconTop GitHub Comments

5reactions
dannymcgeecommented, Jul 10, 2021

The example is already possible with angular. Just donā€™t use ng-content.

Itā€™s a question of API design. As a component author, I want consumers of my component to be able to use it in a way that feels natural to developers who are already comfortable with HTML ā€” i.e., by nesting HTML tags in other HTML tags:

<my-select [(ngModel)]="selectModel">
  <my-option value="foo">Foo</my-option>
  <my-option value="bar">Bar</my-option>
  <my-option value="baz">Baz</my-option>
</my-select>

This API is plainly not possible without using ng-content.

<ng-content> literally just moves the DOM from A to B. And as this is a move and not a copy, you canā€™t have the A in two places. You canā€™t physically move A to B and C.

If you want a copy, just use templates.

Iā€™m not misunderstanding the difference between ng-template and ng-content. The issue I encountered is at the intersection of the two ā€” an ng-content inside of an ng-template. As far as I can tell, this particular combination is not well specified by the docs, but I hope you can agree that given the premises you laid out above (ng-content moves DOM, ng-template copies DOM), itā€™s not unreasonable to infer that placing ng-content inside of an ng-template and then stamping out that template would have the effect of moving the DOM and then copying it.

I understand that wrapping ng-content with ng-template is an unspecified edge case, so ā€œunexpected behaviorā€ is basically par for the course, but Iā€™m not pulling the idea out of thin air ā€” @angular/material uses the same technique in several places to enable similar APIs and behavior, albeit without the cloning that I was trying to achieve in my original example:

3reactions
platon-rovcommented, Jan 18, 2022

Long story short we cannot do the following

<!-- Shows nothing -->
<ng-container *ngTemplateOutlet="content"></ng-container>

<!-- But shown here since it's the last time ng-content is used, i.e. if there will be three attempts - only third will work -->
<ng-container *ngTemplateOutlet="content"></ng-container>

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

And it can be achieved if there template was passed to the component instead of ng-content.

Seems like currently we cannot get such behavior working.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Angular Use same TemplateRef in more than one place at a ...
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Ā ......
Read more >
ngTemplateOutlet: The secret to customisation - Angular inDepth
ngTemplateOutlet is a powerful tool for creating customisable components. It is used by many Angular libraries to enable users to provide custom templates....
Read more >
Customizing A Select Component Using TemplateRef And ...
As you can see, our App component logic is pretty simple - we just define a set of color palettes with the ability...
Read more >
How To Create Reusable Components with NgTemplateOutlet ...
Copy. And a card-or-list-view.component.html template: ... NgTemplateOutlet is a directive that takes a TemplateRef and context and stampsĀ ...
Read more >
ngTemplateOutlet: The secret to customisation | Stephen Cooper
Are you trying to create a customisable component with Angular? TemplateRefs and the ngTemplateOutlet directive could be the secret you areĀ ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found