I'm using Web Components v1.
Suppose two Custom Elements:
parent-element.html
<template id="parent-element"> <child-element></child-element> </template>
child-element.html
<template id="child-element"> <!-- some markup here --> </template>
I'm trying to use connectedCallback
in parent-element
to initialise the entire parent/child DOM structure when it is attached, which requires interaction with methods defined in child-element
.
However, it seems child-element
isn't properly defined at the time connectedCallback
gets fired for customElement
:
parent-element.js
class parent_element extends HTMLElement { connectedCallback() { //shadow root created from template in constructor previously var el = this.shadow_root.querySelector("child-element"); el.my_method(); } }
This will not work, because el
is an HTMLElement
and not a child-element
as expected.
I need a callback for parent-element
once all child custom elements in its template have been properly attached.
The solution in this question does not seem to work; this.parentElement
is null
inside child-element
connectedCallback()
.
ilmiont
the constructor is only ever called once per element so initialisation that needs to happen each time the element is attached to the DOM should be deferred to connectedCallback . the connectedCallback method is best suited to most other initialisation tasks. Any clean up can then happen in disconnectedCallback .
connectedCallback : Invoked each time the custom element is appended into a document-connected element. This will happen each time the node is moved, and may happen before the element's contents have been fully parsed. Note: connectedCallback may be called once your element is no longer connected, use Node.
The CustomElementRegistry interface provides methods for registering custom elements and querying registered elements. To get an instance of it, use the window. customElements property.
The customElements global is used for defining a custom element and teaching the browser about a new tag. Call customElements. define() with the tag name you want to create and a JavaScript class that extends the base HTMLElement .
Use slot elements in your ShadowDOM template.
Build your custom elements in a way so that they can live in any context, like as a child or parent element, without having any dependencies with other custom-elements. This approach will give you a modular design in which you can utilize your custom-elements in any situation.
But you still want to do something whenever a child element is present, like selecting them or calling a method on a child.
To tackle this the <slot>
element has been introduced. With slot elements you can create placeholders inside your ShadowDOM template. These placeholders can be used by simply placing an element inside your custom-element as a child in the DOM. The child element will then be placed inside the position where the <slot>
element is placed.
Slot elements can listen to a unique event called slotchange
. This will be fired whenever an element is (or multiple elements are) placed on the position of the slot
element.
Inside the listener of the event you can access all of the element in the placeholder with the HTMLSlotElement.assignedNodes()
or HTMLSlotElement.assignedElements()
methods. These return an array with the elements placed in the slot
.
Now you can wait for the children to be placed inside the slot and do something with the children that are present.
This way allows you to only manipulate the DOM and leave the ShadowDOM alone and let it do its work. Just like you would do with regular HTML elements.
Yes, the slotchange
event is fired after all connectedCallback
methods of the custom elements have been called. This means no racing conditions or missing setup when listening to the event.
class ParentElement extends HTMLElement { constructor() { super(); this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = ` <h2>Parent Element</h2> <slot></slot> `; console.log("I'm a parent and have slots."); // Select the slot element from the ShadowDOM.. const slot = this.shadowRoot.querySelector('slot'); // ..and listen for the slotchange event. slot.addEventListener('slotchange', (event) => { // Get the elements assigned to the slot.. const children = event.target.assignedElements(); // ..loop over them and call their methods. children.forEach(child => { if (child.tagName.toLowerCase() === 'child-element') { child.shout() } }); }); } connectedCallback() { console.log("I'm a parent and am now connected"); } } customElements.define('parent-element', ParentElement); class ChildElement extends HTMLElement { constructor() { super(); this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = ` <h3>Child Element</h3> `; } connectedCallback() { console.log("I'm a child and am now connected."); } shout() { console.log("I'm a child and placed inside a slot."); } } customElements.define('child-element', ChildElement);
<parent-element> <child-element></child-element> <child-element></child-element> <child-element></child-element> </parent-element>
There is a timing issue with connectedCallback
It gets called, the first time, before any of its custom element children are upgraded. <child-element>
is only an HTMLElement when connectedCallback
is called.
To get at the upgraded child element you need to do it in a timeout.
Run the code below and watch the console output. When we try to call the child's method it fails. Again, this is because of the way Web Components are created. And the timing of when connectedCallback
is called.
But, within the setTimeout
the call to the child's method works. This is because you allowed time for the child element to get upgraded to your custom element.
Kinda stupid if you ask me. I wish there was another function that was called after all children were upgraded. But we work with what we have.
class ParentElement extends HTMLElement { constructor() { super(); this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = '<h2>Parent Element</h2><child-element></child-element>'; } connectedCallback() { let el = this.shadowRoot.querySelector("child-element"); console.log('connectedCallback', el); try { el.childMethod(); } catch(ex) { console.error('Child element not there yet.', ex.message); } setTimeout(() => { let el = this.shadowRoot.querySelector("child-element"); console.log('setTimeout', el); el.childMethod(); }); } } customElements.define('parent-element', ParentElement); class ChildElement extends HTMLElement { constructor() { super(); this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = '<h3>Child Element</h3>'; } childMethod() { console.info('In Child method'); } } customElements.define('child-element', ChildElement);
<parent-element></parent-element>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With