Home > Software engineering >  Cloned Custom HTML Element does not become custom element until it's attached to the main DOM?
Cloned Custom HTML Element does not become custom element until it's attached to the main DOM?

Time:06-25

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";
      });

  • Related