Master the custom element lifecycle, shadow DOM encapsulation and content projection through slots.
Open this lesson in KodokonCustom elements extend the HTML vocabulary: any class inheriting from HTMLElement can be registered in the CustomElementRegistry, as long as its name contains a hyphen (user-badge, never userbadge). Subtle point: if the tag appears in the document before customElements.define is called, it is first created as an unknown element and then upgraded in place - the constructor is then invoked after the fact on a node already present in the tree.
class UserBadge extends HTMLElement {
static observedAttributes = ["name"];
connectedCallback() {
this.render();
}
attributeChangedCallback() {
this.render();
}
render() {
this.textContent = this.getAttribute("name");
}
}
customElements.define("user-badge", UserBadge);The lifecycle holds some traps: attributeChangedCallback only fires for the attributes listed in observedAttributes, and during an upgrade it is called for every attribute already present before connectedCallback. The specification forbids the constructor from inspecting attributes and children, or from adding any: at the moment it runs (via createElement or cloning), the element may be completely empty.
<template id="card-tpl">
<style>
:host { display: block; padding: 1rem; }
::slotted(h2) { margin: 0; }
</style>
<slot name="title"></slot>
<slot></slot>
</template>attachShadow creates an encapsulated subtree: styles do not leak out of it, the page's selectors do not reach into it - only inherited CSS properties (color, font) and custom properties cross the boundary. The content of a <template> lives in a DocumentFragment accessible via .content: its scripts do not run and its images do not load until you clone it into the document. Nodes placed in the light DOM are projected into the <slot> elements: they physically remain in the light DOM, and the slotchange event notifies you of any change in assignment.
class InfoCard extends HTMLElement {
constructor() {
super();
const tpl = document.getElementById("card-tpl");
this.attachShadow({ mode: "open" })
.appendChild(tpl.content.cloneNode(true));
}
}
customElements.define("info-card", InfoCard);Two extensions deserve your attention. Form-associated custom elements: with static formAssociated = true and attachInternals(), your component fully participates in forms (submitted value, native validation, :invalid states). And declarative shadow DOM: <template shadowrootmode=open> attaches a shadow root at parse time, without JavaScript, which makes web components compatible with server-side rendering.
attachShadow({ mode: 'closed' }), what does element.shadowRoot return from the outside?null: only the reference returned by attachShadow gives access to the shadow root<slot> physically live?