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 tag
let namedAttributesMap = dom.body.attributes;
let attributes = [];
for (let attribute of namedAttributesMap) {
attributes.push(`"${attribute.name}"`);
}
attributes = `[${attributes}]`;
// We will inject the 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 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 };
}