The Problem
I am using Angular and its Angular Elements to generate Webcomponents. I want clients to be able to pass these webcomponents icons into slots, so that the client can determine what the icons that get used look like.However, that means having a template that may need to render the slot-contents into multiple places where the icon is needed. Here an example of what I would like to be able to do:
# my-awesome-webcomponent.html
<ul>
<ng-container *ngFor="let entry of entries">
<li>
{{entry.name}}
<slot name="icon-delete"></slot>
</li>
</ng-container>
</ul>
<button>
<slot name="icon-delete"></slot>
Delete entire list?
</button>
# Client using webcomponent - HTML
<my-awesome-webcomponent>
<span slot="icon-delete" class="my-icon-css-class"></span>
</my-awesome-webcomponent>
Which means for a list with n entries inside "my-awesome-webcomponent" I have n+1 occurences of <slot name="icon-delete"></slot> and thus would render <span slot="icon-delete" class="my-icon-css-class"></span> n+1 times.
First attempt
I tried the above and got essentially nowhere.
It is impossible to render the contents of a slot in multiple different places, as per this github issue.. What will happen instead is that the content only gets rendered into the first slot and all later slots are ignored.
Second Attempt
Given that slots themselves appear to not support this, I tried accessing the contents of a slot via JS. I then wanted to just clone whatever content the slot receives (I assumed them to just be Nodes) and append that Node to wherever I need it:
# app-slot-example.ts
@Component({
selector: 'app-slot-example',
templateUrl: './slot-example.component.html',
styleUrls: ['./slot-example.component.scss'],
encapsulation: ViewEncapsulation.ShadowDom,
})
export class SlotExampleComponent {
@ViewChild('icon') icon!: ElementRef;
@ViewChildren('placeholder') placeholders!: QueryList<any>;
iconHTML?: string;
entries = [
{ name: "bla1" },
{ name: "bla2" },
{ name: "bla3" },
{ name: "bla4" },
];
onSlotChange(): void{
console.log("SLOT CHANGED");
this.setIconHTML();
}
private setIconHTML(): void{
const iconNode: HTMLElement = this.icon.nativeElement.childNodes[0].cloneNode();
console.log(iconNode);
this.placeholders.forEach(node => {
const placeholderElement = node.nativeElement;
placeholderElement.appendChild(iconNode);
});
}
}
# app-slot-example.html
<ul>
<ng-container *ngFor="let entry of entries">
<li>
<span #placeholder></span>
{{entry.name}}
</li>
</ng-container>
</ul>
<button>
<span #placeholder></span>
Delete entire list?
</button>
<span #icon>
<slot (slotchange)="onSlotChange()" name="icon-delete"></slot>
</span>
However, that does not work either because there simply is no content inside of a slot. If I am understanding things right slot is literally just a placeholder for the browser to visually render the HTML the client inputs to where the slot is, without actually putting it there.
So what are my options?
This is essentially not answer I'll accept. I am writing this to note that it's a possibility that exists albeit with many drawbacks. You could simply not use slots and instead pass an HTMLElement to @Input.
Code:
This is a solution, but it has multiple drawbacks.