Consider the following scenario where I have a list of custom elements which is dynamically created. The CustomElement
has a custom property displayName
that should do something when set:
class CustomElement extends HTMLElement {
set displayName(v) {
this.querySelector(".name").textContent = v;
}
}
customElements.define("custom-element", CustomElement)
const template = document.querySelector("template");
const list = document.querySelector(".list");
document.querySelector("button").addEventListener("click", () => {
const customElTemplate = template.content.firstElementChild;
console.log("Template constructor: ", customElTemplate.constructor.name);
const el = template.content.firstElementChild.cloneNode(true);
console.log("Cloned el constructor:", el.constructor.name);
el.displayName = "Should has content";
const frag = new DocumentFragment();
frag.appendChild(el);
console.log("Cloned el in DocumentFragment constructor:", el.constructor.name);
list.appendChild(frag);
console.log("Cloned el in Document constructor:", el.constructor.name);
});
custom-element {
display: block;
margin-bottom: .5rem;
border: 1px solid black;
}
<template>
<custom-element><span ></span></custom-element>
</template>
<button>Add Item</button>
<div ></div>
When clicking the button, the console output are:
Template constructor: HTMLElement
Cloned el constructor: HTMLElement
Cloned el in DocumentFragment constructor: HTMLElement
Cloned el in Document constructor: CustomElement
As you can see, the cloned element doesn't become CustomElement
until it's attached to the root document and displayName
is simply a plain property that does nothing.
el.displayName = "Should has content";
won't work even after attaching to a DocumentFragment
. It only works after attaching to the document (i.e. after list.appendChild(frag);
in the above example)
Why is this happening? Is there anyway I can set up the element before attaching it to the main document?
UPDATE: I see there is customElements.upgrade
, but even adding it, nothing is changed, unlike the example provided in the article:
customElements.upgrade(el);
CodePudding user response:
I found out about customElements.upgrade
. Turned out I have to call it after attaching to a shadow DOM:
frag.appendChild(el);
customElements.upgrade(el);
// Now el is CustomElement
console.log(el.constructor.name);
The upgrade() method of the CustomElementRegistry interface upgrades all shadow-containing custom elements in a Node subtree, even before they are connected to the main document.
For the example in my question, I need to:
Attach it to a
DocumentFragment
,Call
customElements.upgrade
Now I can use custom behavior like setting
displayName
.
Working code:
class CustomElement extends HTMLElement {
set displayName(v) {
this.querySelector(".name").textContent = v;
}
}
customElements.define("custom-element", CustomElement)
const template = document.querySelector("template");
const list = document.querySelector(".list");
document.querySelector("button").addEventListener("click", () => {
const customElTemplate = template.content.firstElementChild;
console.log("Template constructor: ", customElTemplate.constructor.name);
const el = template.content.firstElementChild.cloneNode(true);
console.log("Cloned el constructor:", el.constructor.name);
const frag = new DocumentFragment();
frag.appendChild(el);
customElements.upgrade(el);
console.log("Cloned el in DocumentFragment constructor:", el.constructor.name);
el.displayName = "Should has content";
list.appendChild(frag);
console.log("Cloned el in Document constructor:", el.constructor.name);
});
custom-element {
display: block;
margin-bottom: .5rem;
border: 1px solid black;
}
<template>
<custom-element><span ></span></custom-element>
</template>
<button>Add Item</button>
<div ></div>
CodePudding user response:
Yes, DOM operations can only be done when the (Custom) Element is in the DOM
Note: I am saying DOM, not shadowDOM. There is no shadowDOM at all in your code.
A DocumentFragment
is not a DOM.
Why the template.content.firstElementChild
to select/create your Custom Element?
That makes <template>
a very expensive container.
document.createElement('custom-element')
will create a Custom Element (but it is still not a DOM Element at this point.
I don't know your use-case; but you code could be condensed to:
<button>Add Item</button>
<div ></div>
<script>
customElements.define("custom-element", class extends HTMLElement {
connectedCallback() {
this.innerHTML = `<span ></span>`;
}
set displayName(v) {
this.querySelector(".name").textContent = v;
}
})
document.querySelector("button").addEventListener("click", () => {
let newElement = document.createElement("custom-element");
document.querySelector(".list").append(newElement);
newElement.displayName = "Should have content";
});
</script>
<style>
custom-element {
display: block;
margin-bottom: .5rem;
border: 1px solid black;
}
</style>
If you do need that <template>
, because you want your HTML in the HTML file:
<template id="CUSTOM-ELEMENT">
<span ></span>
</template>
<button>Add Item</button>
<div ></div>
<script>
customElements.define("custom-element", class extends HTMLElement {
connectedCallback() {
this.append(document.getElementById(this.nodeName).content.cloneNode(true));
}
set displayName(v) {
this.querySelector(".name").textContent = v;
}
})
document.querySelector("button").addEventListener("click", () => {
let newElement = document.createElement("custom-element");
document.querySelector(".list").append(newElement);
newElement.displayName = "Should have content";
});
</script>
<style>
custom-element {
display: block;
margin-bottom: .5rem;
border: 1px solid black;
}
</style>
Note: the use of (the often more powerful, because it can append multiple elements) append
Not available in IE11.
appendChild
is for when you need its return value.
So you could write:
document.querySelector("button").addEventListener("click", () => {
document
.querySelector(".list")
.appendChild(document.createElement("custom-element"))
.displayName = "Should have content";
});