HTML Custom Elements
Updated: 14 November 2023
Below is a small example showing an HTML Custom Element that works as a markdown input and preview. The HTML file renders the element using the respective tag:
index.html
1<!DOCTYPE html>2<html lang="en">3 <head>4 <meta charset="UTF-8" />5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />6 <title>Web Components Playground</title>7 <!-- Custom element script needs to be deferred due to the initialization lifecycle -->8 <script src="main.js" defer></script>9 <!-- Showdown used for markdown conversion -->10 <script src="https://cdn.jsdelivr.net/npm/showdown@2.1.0/dist/showdown.min.js"></script>11 </head>12 <body>13 <h1>A little markdown editor</h1>14
15 <app-contenteditable16 onchange="console.log(event)"17 label="Custom Element Input"18 ></app-contenteditable>19 </body>20</html>
And the Javascript implementation is as follows:
- TS Check declaration used to get type checking on the JS file with the relevant JSDoc
- JS Doc used to provide better class level type intferrence
- Private class members in JS using
#
- The HTML
html=String.raw
used for syntax highlighting of HTML strings
main.js
1// @ts-check2
3const html = String.raw4
5/**6 * @typedef {Object} Converter7 * @property {(markdown: string) => string} makeHtml8 */9
10/**11 * @typedef {new () => Converter} ConverterConstructor12 */13
14/**15 * @typedef {Object} Showdown16 * @property {ConverterConstructor} Converter17 */18
19/** @type {Showdown} */20const showdown = window["showdown"]21
22class MarkdownPreviewElement extends HTMLElement {23 static selector = "app-contenteditable"24
25 #converter = new showdown.Converter()26
27 /** @type {string} */28 get #label() {29 return this.getAttribute("label") || ""30 }31
32 /** @type {string} */33 get #value() {34 return this.getAttribute("value") || ""35 }36
37 /** @type {HTMLDivElement} */38 #wrapper39
40 get #input() {41 const el = /** @type {HTMLTextAreaElement} */ (42 this.shadowRoot?.getElementById("input")43 )44
45 return el46 }47
48 get #output() {49 const el = /** @type {HTMLDivElement} */ (50 this.shadowRoot?.getElementById("output")51 )52
53 return el54 }55
56 #onchange() {57 const markdown = this.#input.value58 const html = this.#converter.makeHtml(markdown)59
60 this.#output.innerHTML = html61
62 const event = new CustomEvent("change", {63 detail: {64 markdown,65 html,66 },67 })68
69 this.onchange?.(event)70 }71
72 /** @type {string} */73 get #content() {74 return html`75 <h2>Markdown</h2>76 <textarea id="input" value="${this.#value}"></textarea>77 <h2>HTML</h2>78 <div id="output"></div>79 `80 }81
82 constructor() {83 super()84 this.#wrapper = document.createElement("div")85 this.#wrapper.className = MarkdownPreviewElement.selector86
87 this.#wrapper.innerHTML = this.#content88
89 const shadow = this.attachShadow({ mode: "open" })90 shadow.appendChild(this.#wrapper)91
92 this.#input.addEventListener("input", () => this.#onchange())93 this.#output.innerHTML = this.#converter.makeHtml(this.#value)94 }95}96
97customElements.define(MarkdownPreviewElement.selector, MarkdownPreviewElement)