async function init() { Array.from(document.querySelectorAll("link[rel='import']")).forEach(async (link) => { fetch(link.getAttribute("href")).then(async (response) => { response.text().then(async (html) => { await mountComponentFromHTML(html); if (postUpgrade) postUpgrade(); }); }); }); } function upgradeCustomElements() { Array.from(document.querySelectorAll("custom-element")).forEach(async (element) => { fetch(element.getAttribute("src")).then(async (response) => { response.text().then(async (html) => { let component = await mountComponentFromHTML(html); let outerHTML = element.outerHTML.replaceAll( element.tagName.toLowerCase(), component.name || element.getAttribute("name") ); let customElement = new DOMParser().parseFromString(outerHTML, "text/html").body.firstElementChild; customElement.removeAttribute("src"); element.parentNode.insertBefore(customElement, element.nextSibling); element.remove(); if (postUpgrade) postUpgrade(); }); }); }); } init(); upgradeCustomElements(); // We can take the HTML, parse it, extract parts and re-assemble it inside the CustomElement. async function mountComponentFromHTML(html) { let dom = new DOMParser().parseFromString(html, "text/html"); // We use the of the HTML as the name for the component let name = dom.head.querySelector("title").innerText; // We get the attributes from the <body> tag let namedAttributesMap = dom.body.attributes; let attributes = []; for (let attribute of namedAttributesMap) { attributes.push(`"${attribute.name}"`); } attributes = `[${attributes}]`; // We will inject the <head> into the Shadow DOM so that external resources like fonts are loaded let headText = dom.head.innerHTML; // We will later inject the script (this demo assumes only a one script tag per file) let script = dom.body.querySelector("script"); let scriptText = ""; if (script) scriptText = script.innerText; // We will later inject the style (this demo assumes only a one style tag per file) let style = dom.body.querySelector("style"); let styleText = ""; if (style) { style.innerText; } // In order to get raw "template", we’ll remove the style and script tags. // This is a limitation / convention of this demo. if (script) script.remove(); if (style) style.remove(); // The <body> is our template let template = dom.body.outerHTML; let construct = `customElements.define( '${name}', class HTMLComponent extends HTMLElement { constructor() { super(); var shadow = this.attachShadow({ mode: "open" }); let head = document.createElement("head"); head.innerHTML = \`${headText}\`; shadow.appendChild(head); let body = document.createElement("body"); body.innerHTML = \`${template}\`; shadow.appendChild(body); let style = document.createElement("style"); style.innerText = \`${styleText}\`; body.appendChild(style); new Function("document", "attributes", \`${scriptText}\`)( this.shadowRoot, this.attributes ); } static get observedAttributes() { return ${attributes}; } attributeChangedCallback(name, oldValue, newValue) { this.shadowRoot.dispatchEvent( new CustomEvent("attribute.changed", { composed: true, detail: { name, oldValue, newValue, value: newValue } }) ); } } ); `; await import(`data:text/javascript;charset=utf-8,${encodeURIComponent(construct)}`); return { name }; }