Web Component

Author:Gao
Created At:2022-04-07

Concepts

Class CustomElementRegistry

  • define The define() method of the CustomElementRegistry interface defines a new custom element. There are two types of custom elements you can create:
    • Autonomous custom element: Standalone elements; they don’t inherit from built-in HTML elements.
    • Customized built-in element: These elements inherit from — and extend — built-in HTML elements.
customElements.define(name, constructor, options);
  • upgrade 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.
customElements.upgrade(root);
  • whenDefined The whenDefined() method of the CustomElementRegistry interface returns a Promise that resolves when the named element is defined.
customElements.whenDefined(name): Promise<CustomElementConstructor>;

Example

// Create a class for the element
class WordCount extends HTMLParagraphElement {
  constructor() {
    // Always call super first in constructor
    super();

    // count words in element's parent element
    var wcParent = this.parentNode;

    function countWords(node){
      var text = node.innerText || node.textContent
      return text.split(/\s+/g).length;
    }

    var count = 'Words: ' + countWords(wcParent);

    // Create a shadow root
    var shadow = this.attachShadow({mode: 'open'});

    // Create text node and add word count to it
    var text = document.createElement('span');
    text.textContent = count;

    // Append it to the shadow root
    shadow.appendChild(text);

    // Update count when element content changes
    setInterval(function() {
      var count = 'Words: ' + countWords(wcParent);
      text.textContent = count;
    }, 200)

  }
}

// Define the new element
customElements.define('word-count', WordCount, { extends: 'p' });

Real World Example

import { LitElement, html, css, unsafeCSS } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { until } from 'lit/directives/until.js';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';

import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkGfm from 'remark-gfm';
import remarkRehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';
import rehypeHighlight from 'rehype-highlight';

import mermaid from 'mermaid';

import github from './github.css';
import light from './atom-one-light.css';
import dark from './atom-one-dark.css';

@customElement('remark-element')
class RemarkElement extends LitElement {
  static override styles = css`
    :host {
      display: flex;
      flex-direction: column;
    }
    li > * {
      display: inline;
    }
    li > ul {
      display: block;
    }
    ${unsafeCSS(github)}
    @media (prefers-color-scheme: dark) {
      ${unsafeCSS(dark)}
    }
    @media (prefers-color-scheme: light) {
      ${unsafeCSS(light)}
    }
  `;

  @property({ type: Boolean, attribute: true, reflect: true })
  debug: boolean = false;

  private _content: string | undefined = undefined;
  set content(val: string | undefined) {
    const oldVal = this._content;
    this._content = val;
    this.requestUpdate('content', oldVal);
  }
  get content() {
    return this._content;
  }

  private _mid: string;
  private _fragement: string;

  constructor() {
    super();

    const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    mermaid.initialize({
      startOnLoad: false,
      theme: isDark ? 'dark' : 'default',
    });
    this._mid = this.id || `t${+new Date()}`;
    this._fragement = '';
  }

  private _generate() {
    const content = this.content ?? this.innerHTML;

    return unified()
      .use(remarkParse)
      .use(remarkGfm)
      .use(remarkRehype)
      .use(rehypeHighlight, { ignoreMissing: true })
      .use(rehypeStringify)
      .process(content)
      .then((vFile) => {
        this._fragement = String(vFile);
        return unsafeHTML(String(vFile));
      });
  }

  private _do_updated() {
    const els: NodeListOf<HTMLElement> = this.renderRoot.querySelectorAll(
      'code.language-mermaid',
    );
    const fragement = document.createElement('div');
    fragement.innerHTML = this._fragement;
    const contentEls: NodeListOf<HTMLElement> = fragement.querySelectorAll(
      'code.language-mermaid',
    );

    for (let i = 0, len = els.length; i < len; i += 1) {
      const boxId = `mermaid-${this._mid}-${i}`;
      let box = document.getElementById(boxId);
      if (!box) {
        box = document.createElement('div');
        box.id = boxId;
        document.body.append(box);
        box.style.display = 'none';
      }
      const el = els[i];
      const txt = contentEls[i].innerText;
      if (this.debug) {
        console.log(txt);
      }
      const cb = (svgGraph: string) => {
        if (this.debug) {
          console.log(svgGraph);
        }
        el.innerHTML = svgGraph;
      };
      const decodedTxt = this._decodeEntities(txt);
      if (this.debug) {
        console.log(decodedTxt);
      }
      mermaid.mermaidAPI.render(box.id, decodedTxt, cb);
    }
  }

  override updated() {
    setTimeout(() => this._do_updated(), 1000 / 60);
  }

  // override attributeChangedCallback(...args) {
  //   if (this.debug) {
  //     console.log(...args);
  //   }
  //   this.requestUpdate(...args);
  // }

  override render() {
    const md = this._generate();

    return html`${until(md)}`;
  }

  private _decodeEntities(txt: string): string {
    return txt.replace(/&gt;/gi, '>').replace(/&lt;/gi, '<');
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'remark-element': RemarkElement;
  }
}

Relative