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:

  1. 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.

  2. 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.

  3. 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:

  1. The <template> tag is used to define the HTMLTemplateElement object. This object defines a single content property with a value DocumentFragment of all the child nodes of the <template>.

  2. The content of the <template> allows you to define reusable chunks of HTML that can be cloned and inserted into the document multiple times.

  3. 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:

  1. 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.

  2. 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.
  3. To make a custom element available in a page, call the define()method of Window.customElements. The custom element tag name must include a hyphen.

  4. 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 the attributeChangedCallback() 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:

  1. 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's shadowRoot 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.
  2. 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 the slot attribute.
  3. 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.
  4. 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.
  5. 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);