Create expectations
When it comes to custom elements, once you create a class and connect the piece to a valid tag name, there are different ways to see the content take shape. Some which demand more attention than others.
As a small example, consider a component promising a particle system: particle-push. Following the mentioned steps, you define the custom structure with a class.
class ParticlePush extends HTMLElement {
constructor() {
super();
}
} And lean on the customElements module, connecting the class to the element with the define function.
customElements.define('particle-push', ParticlePush); From this point, the most straightforward option is to include the element in markup, with an opening and closing tag.
<particle-push></particle-push> But there are indeed alternatives. As with other HTML elements, one way is to go through the document object, and rely on the createElement method.
Create the element passing the name as argument.
const element = document.createElement('particle-push'); And append the node in the desired context.
document.body.appendChild(element); Be warned, however, that the code may fail in unprompted manners. This may so happen if you were to author the markup directly in the constructor function.
super();
this.innerHTML = `<button>Spawn particles</button>...`; In this instance, the script will break with a specific error message.
There’s nothing inherently wrong with the custom element — this time; the issue is not in the scope of the custom elements API either, but an expectation of createElement. The specification lists a series of standards you need to respect when using the function. Among the rules, the one stressing the lack of attributes and children in the constructor explains the mistake.
To fix the issue, the standard advice is to defer the logic in the separate connectedCallback lifecycle function. This one is executed not as the element is created, but as the node is inserted in the DOM.
connectedCallback() {
// this.innerHTML = `...`;
} The code will work, but you need to be careful still. The constructor function runs once, for the entire existence of the element, but the different callback may run more often. For this reason you need to refine the logic, for example adding a safeguard to test for child nodes.
if (this.children.length === 0) {
// this.innerHTML = `...`;
} Past the two methods, in markup or through the document object, there are even more ways to complete the task and delight in the particles. And these do not suffer from the same expectations of the creating function.
Rather cheekily, you can add the element through the innerHTML property, in a supporting container.
document.body.innerHTML += `<particle-push></particle-push>`; What is more, with a syntax that in hindsight might be close to being obvious, you can spawn an instance of the defined class with the new keyword.
const element = new ParticlePush(); You can add the node with the same appending function used when creating the element.
document.body.appendChild(element); And rest assured that, JavaScript being available, you will see the component in full.
Without entering in a discussion of a much broader topic, there is one more way to satisfy everybody’s expectations, for a custom element and the document.createElement function, and that is with the Shadow DOM. With it you create a separate document tree, which manages its own child elements.
Even in the constructor, you can therefore store a reference to the new document, and complete the markup immediately.
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `...`; And be even more sure that the code will work, for once and for all.