问题
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
回答1:
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>
回答2:
After some more work, I have a solution of sorts.
Of course this.parentElement
doesn't work in the child element; it's in the root of the shadow DOM!
My current solution, which is alright for my specific scenario, is as follows:
parent-element.js
init() {
//Code to run on initialisation goes here
this.shadow_root.querySelector("child-element").my_method();
}
child-element.js
connectedCallback() {
this.getRootNode().host.init();
}
So in child element, we get the root node (template shadow DOM) and then its host, the parent element, and call init(...)
, at which point the parent can access the child and it's fully defined.
This solution isn't ideal for a couple of reasons, so I'm not marking it as accepted.
1) If there are multiple children to wait for, or deeper nesting, it's going to get more complicated to orchestrate the callbacks.
2) I'm concerned about the implications for child-element
, if I want to use this element in a standalone capacity (i.e. somewhere else, entirely separate from being nested in parent-element
) I will have to modify it to explicitly check whether getRootNode().host
is an instance of parent-element
.
So this solution works for now, but it feels bad and I think there needs to be a callback that fires on the parent when its entire DOM structure, including nested custom elements in its shadow DOM, is initialised.
回答3:
Use slot elements in your ShadowDOM template.
It is a bad practice to add custom-elements to the ShadowDOM template. Every custom-element must be able to live without dependencies from other custom-elements in the DOM. Using native HTML elements in the ShadowDOM is no problem for they will always work.
Slot elements
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 in the DOM.
But how do you know if a placeholder has been filled with an element?
Slot elements can listen to an unique event called slotchange
. This will be fired whenever an element (or multiple elements) is 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
.
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. I have slots.");
// Select the slot element and listen for the slotchange event.
const slot = this.shadowRoot.querySelector('slot');
slot.addEventListener('slotchange', (event) => {
const children = event.target.assignedElements();
children.forEach(child => child.shout());
});
}
}
customElements.define('parent-element', ParentElement);
class ChildElement extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = '<h3>Child Element</h3>';
}
shout() {
console.log("I'm a child, 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>
回答4:
If you want to avoid having any visual glitches caused by the delay of a setTimeout, you could use a MutationObserver.
class myWebComponent extends HTMLElement
{
connectedCallback() {
let instance = this;
let childrenConnectedCallback = () => {
let addedNode = instance.childNodes[(instance.childNodes.length - 1)];
//callback here
}
let observer = new MutationObserver(childrenConnectedCallback);
let config = { attributes: false, childList: true, subtree: true };
observer.observe(instance, config);
//make sure to disconnect
setTimeout(() => {
observer.disconnect();
}, 0);
}
}
回答5:
We've run into very related problems, with children being unavailable in the connectedCallback
of our custom elements (v1).
At first we tried to fix the connectedCallback
with a very complicated approach that's also being used by the Google AMP team (a combination of mutationObserver
and checking for nextSibling
) which ultimately led to https://github.com/WebReflection/html-parsed-element
This unfortunately created problems of its own, so we went back to always enforcing the upgrade case (that is, including the script that registers the custom elements only at the end of the page).
回答6:
document.addEventListener('DOMContentLoaded', defineMyCustomElements);
You can delay defining your classes until after the dom has been loaded.
来源:https://stackoverflow.com/questions/48663678/how-to-have-a-connectedcallback-for-when-all-child-custom-elements-have-been-c