[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.
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:
- Created 3 years ago
- Reactions:15
- Comments:12 (5 by maintainers)
Top GitHub Comments
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:
This API is plainly not possible without using
ng-content
.Iām not misunderstanding the difference between
ng-template
andng-content
. The issue I encountered is at the intersection of the two ā anng-content
inside of anng-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 placingng-content
inside of anng-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
withng-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:Long story short we cannot do the following
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.