Web Components
Many developers use web frameworks such as React and Angular that support reusable user interface components. Web components is a browser-native alternative to those frameworks that allows JavaScript to extend HTML with new tags that work as self-contained, reusable UI components.
Document fragment
DocumentFragment
is a lightweight container in the DOM that can hold a portion of the document structure (like a subtree) without having a parent. It's used primarily to create, manipulate, and insert multiple elements into the DOM in a more efficient way. DocumentFragment
allows you to batch changes to the DOM, which can significantly improve performance when adding, moving, or modifying large groups of nodes. Key characteristics:
-
A
DocumentFragment
doesn't have the overhead associated with full-fledged DOM nodes. It doesn't have a parent and isn't part of the main DOM tree until its contents are appended to the document. -
Operations performed on a
DocumentFragment
are done in memory and not on the live DOM, which avoids the reflow and repaint cycles that happen when modifying the DOM directly. -
When a
DocumentFragment
is appended to the DOM, its children are appended, not the fragment itself. The fragment is essentially a container that "disappears" once its children are inserted into the DOM.
const fragment = document.createDocumentFragment();
// Create multiple list items and append them to the fragment
for (let i = 1; i <= 5; i++) {
const li = document.createElement("li");
li.textContent = `Item ${i}`;
fragment.append(li);
}
// Append the fragment to the DOM
const itemList = document.querySelector("#item-list");
itemList.append(fragment);
HTML templates
HTML Templates is a web standard that allows developers to define reusable HTML fragments that are not rendered when the page loads but can be instantiated later using JavaScript. This is particularly useful for creating reusable components and dynamically generating content. Key concepts:
-
The
<template>
tag is used to define theHTMLTemplateElement
object. This object defines a singlecontent
property with a valueDocumentFragment
of all the child nodes of the<template>
. -
The content of the
<template>
allows you to define reusable chunks of HTML that can be cloned and inserted into the document multiple times. -
The content inside the
<template>
tag is not rendered immediately when the page loads, which leads to high performance.
<template id="my-template">
<style>
.greeting {
color: green;
font-weight: bold;
}
</style>
<div class="greeting">Hello, Template!</div>
</template>
const template = document.querySelector("#my-template");
const templateContent = template.content.cloneNode(true);
const container = document.querySelector("#template-container");
container.append(templateContent);
Custom elements
Custom Elements is a web standard that allows developers to create new HTML tags and define their behavior, structure, and styling. Custom Elements enable you to create reusable, encapsulated HTML elements that can be used just like standard HTML tags. Key concepts:
-
You define a custom element by creating a class that extends the
HTMLElement
class. This class contains methods that describe the behavior and lifecycle of the element. -
Custom elements have several lifecycle callbacks that allow you to run code at different stages of the element's life:
connectedCallback()
: Called when the element is added to the document.disconnectedCallback()
: Called when the element is removed from the document.attributeChangedCallback()
: Called when one of the element's attributes is added, removed, or changed.adoptedCallback()
: Called when the element is moved to a new document.
-
To make a custom element available in a page, call the
define()
method ofWindow.customElements
. The custom element tag name must include a hyphen. -
If a custom element class defines a static
observedAttributes
property whose value is an array of attribute names, and if any of the named attributes are set (or changed) on an instance of the custom element, the browser will invoke theattributeChangedCallback()
method.
<p>
A basic circle <inline-circle></inline-circle> and a large red
circle <inline-circle color="red" diameter="2em"></inline-circle> appear
inline.
</p>
class InlineCircle extends HTMLElement {
constructor() {
super();
}
// This method is called when custom element is inserted into document
connectedCallback() {
this.style.display = "inline-block";
this.style.borderRadius = "50%";
this.style.border = "solid black 1px";
if (!this.style.width) {
this.style.width = "1em";
this.style.height = "1em";
}
}
// A change to these atributes will invoke attributeChangedCallback()
static get observedAttributes() {
return ["diameter", "color"];
}
// This method is called when one of attributes listed above is changed
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case "diameter":
this.style.width = newValue;
this.style.height = newValue;
break;
case "color":
this.style.backgroundColor = newValue;
break;
}
}
// Ensure JS properties are in sync with element's attributes
get diameter() {
return this.getAttribute("diameter");
}
set diameter(newValue) {
this.setAttribute("diameter", newValue);
}
get color() {
return this.getAttribute("color");
}
set color(newValue) {
this.setAttribute("color", newValue);
}
}
// Define the new element
customElements.define("inline-circle", InlineCircle);
Shadow DOM
Shadow DOM is a web standard that encapsulates a part of the DOM tree in a way that keeps it isolated from the main document DOM. This allows for better component-based design by providing encapsulation and style isolation, making it an essential part of creating reusable web components. Key points:
-
The structure within a Shadow DOM is independent of the main document's DOM.
- Create shadow: To turn a light DOM element into a shadow host, call its
attachShadow()
method, passing{mode: "open"}
as the only argument. This method returns a shadow root object and also sets that object as the value of the host'sshadowRoot
property. - Shadow Root: A shadow root is the entry point to the shadow DOM. The shadow root object is a DocumentFragment.
- Shadow Tree: The DOM tree inside the shadow root. Elements in the shadow tree are scoped to the shadow DOM.
- Create shadow: To turn a light DOM element into a shadow host, call its
-
Shadow DOM slots are placeholders inside the shadow DOM where content from the light DOM can be inserted.
- Default Slot: If a slot does not have a name, it is considered a default slot. Content in the light DOM that does not have a
slot
attribute will be placed into the default slot. - Named Slot: Slots can have names specified by the
name
attribute. Content in the light DOM can specify which named slot it should go into using theslot
attribute.
- Default Slot: If a slot does not have a name, it is considered a default slot. Content in the light DOM that does not have a
-
Light DOM children are the elements of the document's main DOM tree that are passed to a custom element by its user.
- Content Projection: Light DOM children are projected into the shadow DOM using slots. This allows the custom element to control the placement and styling of user-provided content.
- Fallback Content: If no light DOM children are assigned to a slot, the slot's fallback content (defined within the shadow DOM) is used.
-
Styles within a shadow DOM are encapsulated.
- Local Styles: Styles defined inside the shadow DOM only apply to elements within the shadow tree. They do not affect the main document or other shadow DOMs.
- Global Styles: Styles in the main document do not affect the shadow DOM.
-
Scripts can interact with the shadow DOM, but they do so in an encapsulated manner.
- Encapsulation: Scripts inside a shadow DOM only affect elements within that shadow DOM.
- Accessing Shadow DOM: Scripts in the main document can access and manipulate the shadow DOM, but they must do so explicitly (e.g., by querying the shadow root).
<search-box placeholder="Search...">
<span slot="search-icon">🐶</span>
<span slot="cancel-icon">❌</span>
</search-box>
export default class SearchBox extends HTMLElement {
constructor() {
super();
// Create HTML template
const template = document.createElement("template");
template.innerHTML = `
<style>
/* Applies to the light DOM and can be overriden by the user */
:host {
display: inline-block;
border: solid black 1px;
border-radius: 5px;
padding: 4px 6px;
}
:host([hidden]) {
display: none;
}
:host([disabled]) {
opacity: 0.5;
}
:host([focused]) {
box-shadow: 0 0 2px 2px #6AE;
}
/* Applies to elements in Shadow DOM */
input {
border-width: 0;
outline: none;
font: inherit;
background: inherit;
}
slot {
cursor: default;
user-select: none;
}
</style>
<div>
<slot name="search-icon">\u{1f50d}</slot>
<input type="text" id="input" />
<slot name="cancel-icon">\u{2573}</slot>
</div>
`;
// Create shadow DOM structure
this.attachShadow({ mode: "open" });
this.shadowRoot.append(template.content.cloneNode(true));
// Set the "focused" attribute when in focus
this.input = this.shadowRoot.querySelector("#input");
this.input.onfocus = () => {
this.setAttribute("focused", "");
};
// Remove the "focused" attribute when on blur
this.input.onblur = () => {
this.removeAttribute("focused");
};
// Trigger a "search" event when user clicks on magnifying glass
// or when input field fires a "change" event
const searchSlot = this.shadowRoot.querySelector(
'slot[name="search-icon"]'
);
searchSlot.onclick = this.input.onchange = (e) => {
e.stopPropagation();
if (this.disabled) return;
this.dispatchEvent(
new CustomEvent("search", {
detail: this.input.value,
})
);
};
// Trigger a "clear" event when the user klicks on X
const cancelSlot = this.shadowRoot.querySelector(
'slot[name="cancel-icon"]'
);
cancelSlot.onclick = (e) => {
e.stopPropagation();
if (this.disabled) return;
const clearEvent = new CustomEvent("clear", { cancelable: true });
this.dispatchEvent(clearEvent);
if (!clearEvent.defaultPrevented) {
this.input.value = "";
}
};
}
// A change to these atributes will invoke attributeChangedCallback()
static get observedAttributes() {
return ["disabled", "placeholder", "size", "value"];
}
// This method is called when one of attributes listed above is changed
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case "disabled":
this.input.disabled = newValue !== null;
break;
case "placeholder":
this.input.placeholder = newValue;
break;
case "size":
this.input.size = newValue;
break;
case "value":
this.input.value = newValue;
break;
}
}
// Ensure JS properties are in sync with element's attributes
get placeholder() {
return this.getAttribute("placeholder");
}
set placeholder(newValue) {
return this.setAttribute("placeholder", newValue);
}
get size() {
return this.getAttribute("size");
}
set size(newValue) {
return this.setAttribute("size", newValue);
}
get value() {
return this.getAttribute("value");
}
set value(newValue) {
return this.setAttribute("value", newValue);
}
get disabled() {
return this.hasAttribute("disabled");
}
set disabled(newValue) {
if (newValue) {
this.setAttribute("disabled", true);
} else {
this.removeAttribute("disabled");
}
}
get hidden() {
return this.hasAttribute("hidden");
}
set hidden(newValue) {
if (newValue) {
this.setAttribute("hidden", true);
} else {
this.removeAttribute("hidden");
}
}
}
customElements.define("search-box", SearchBox);