Within a custom component defined with <template>
and <slot>
tags, a child element can get the parent element that is the custom component, and then use this element reference to access class defined vars and functions.
However, how can a child element know which element is the custom component?
The template itself is contained in a shadow DOM. When rendering a custom component on the page, this shadow DOM is cloned and attached into the custom component as its host.
Elements that are slotted in to the custom component however can't seem to tell the node tree it is attached to came from a shadow DOM node that was cloned, and can't tell which element in the current DOM tree is the top level custom component unless it iterates through each parentNode
to check if they have a shadowRoot
.
What is the way to let a child element inside a custom component access the custom component's class variables and functions?
customElements.define("my-comp", class extends HTMLElement {
creationTimestamp;
constructor() {
super();
let template = document.getElementById('my-template');
let templateContent = template.content;
const shadowRoot = this.attachShadow({
mode: 'open'
});
shadowRoot.appendChild(templateContent.cloneNode(true));
this.creationTimestamp = Date.now();
}
})
<html>
<template id="my-template">
I'm a new custom component using slots with value <slot name="slot1">default</slot>
<button onclick="console.log(this.parentNode.host.creationTimestamp)">Print timestamp at Template level</button>
</template>
<my-comp>
<span slot="slot1">
slotted value
<div>
<button onclick="console.log(this.parentNode.parentNode.parentNode.creationTimestamp)">Print timestamp</button>
</div>
</span>
</my-comp>
</html>
CodePudding user response:
Now I understand. You want to prevent:
let timeStamp = this.parentNode.parentNode.parentNode.creationTimestamp;
There are multiple StackOverflow answers tackling this:
Let the child find a parent
In control: child
mimic standard JS.closest(selector)
which does not pierce shadowRoots!
with a customclosestElement(selector)
function:
CodePudding user response:
Like @connexo stated, a child should not concern itself about it's children. Do it the other way around. Let the parent decide what to do with the child.
You already incorporated two approaches with the buttons, one with a button in the Shadow DOM and one button that is assigned to a slot.
Template
The former can be done by selecting the
<button>
element from theshadowRoot
document in the custom element. When you have the element, add an event listener to it and pass the method of the custom element that you want to call.customElements.define("my-comp", class extends HTMLElement { creationTimestamp; constructor() { super(); let template = document.getElementById('my-template'); let templateContent = template.content; const shadowRoot = this.attachShadow({ mode: 'open' }); shadowRoot.appendChild(templateContent.cloneNode(true)); this.creationTimestamp = Date.now(); } get button() { return this.shadowRoot.querySelector('button'); } connectedCallback() { this.button.addEventListener('click', this.logCreationTimestamp); } logCreationTimestamp = () => { console.log(this.creationTimestamp); } })
<template id="my-template"> <button>Print timestamp at Template level</button> </template> <my-comp></my-comp>
Slot
The latter is a bit more complex, but more flexible. It requires you to use the
slotchange
event the<slot>
element in your template. That event will let you know when any element has been assigned (or removed) from the slot.When the event is fired, check for the
assignedElements
in the slot and add event listeners, just like in the first example.customElements.define("my-comp", class extends HTMLElement { creationTimestamp; constructor() { super(); let template = document.getElementById('my-template'); let templateContent = template.content; const shadowRoot = this.attachShadow({ mode: 'open' }); shadowRoot.appendChild(templateContent.cloneNode(true)); this.creationTimestamp = Date.now(); const slot = this.shadowRoot.querySelector('slot[name="slot1"]'); slot.addEventListener('slotchange', () => { const elements = slot.assignedElements(); for (const element of elements) { if (element.tagName === 'BUTTON') { element.addEventListener('click', this.logCreationTimestamp); } } }); } logCreationTimestamp = () => { console.log(this.creationTimestamp); } })
<template id="my-template"> <span > <div> <slot name="slot1">default</slot> </div> </span> </template> <my-comp> <button slot="slot1">Print timestamp</button> </my-comp>