Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save simonw/2df444ce0bd75c8bf91beb7a6516ba5b to your computer and use it in GitHub Desktop.
Save simonw/2df444ce0bd75c8bf91beb7a6516ba5b to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Single File Web Component</title>
<style>
body {
background-color: #eee;
font-family: Helvetica, sans-serif;
}
h1 {
color: blue;
background-color: pink;
}
</style>
</head>
<body>
<template id="single-file">
<style>
/*
These styles affect only content inside the shadow DOM.
Styles in the outside document mostly do not affect these,
with the exception of inheritable styles such as color,
font and line-height.
*/
h1 {
color: red;
}
</style>
<h1>Hello world (web component)</h1>
<!--
This code still works if you remove the type="module" parameter.
Using that parameter enables features such as 'import ... from'
More importantly it stops variables inside the script tag from
leaking out to the global scope - if you remove type="module"
from here then the HelloWorld class becomes visible.
-->
<script type="module">
class HelloWorld extends HTMLElement {
constructor() {
/*
If you remove the call to super() here Firefox shows an error:
"Uncaught ReferenceError: must call super constructor before
using 'this' in derived class constructor'"
*/
super();
const template = document.getElementById("single-file");
/*
mode: 'open' means you are allowed to access
document.querySelector('hello-world').shadowRoot to get
at the DOM inside. Set to 'closed' and the .shadowRoot
property will return null.
*/
this.attachShadow({ mode: "open" }).appendChild(
template.content.cloneNode(true)
);
/*
template.content is a 'DocumentFragment' array.
template.content.cloneNode() without the true performs
a shallow clone, which provides a empty DocumentFragment
array.
template.content.cloneNode(true) provides one with 6 items
*/
}
connectedCallback() {
// https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#using_the_lifecycle_callbacks
console.log("Why hello there 👋");
}
}
customElements.define("hello-world", HelloWorld);
</script>
</template>
<h1>This is not a web component</h1>
<hello-world></hello-world>
<script>
const sf = document.getElementById("single-file");
/*
Before executing this line, sf.content.lastElementChild
is the <script type="module"> element hidden away inside
the <template> - we remove it from the template here and
append it to the document.body, causing it to execute in
the main document and activate the <hello-world> tag.
*/
document.body.appendChild(sf.content.lastElementChild);
</script>
</body>
</html>
@kristoferjoseph
Copy link

@zellyn

Do you understand why the script is inside the <template> element, rather than immediately following it? Seems like that would avoid the need to append it onto the body.

This was an idea about how to ship .html files that work as single file components.
Everything a web component needs would be shipped together as a template tag.
The host HTML page then would only need to take the script tags from all the templates in the page and append them to it's body to instantiate every web component on the page.

This is a way to illustrate how to use technology that already exists in the browser to eliminate the need for a complicated build system or framework. As simple as a single file component could get.

Obviously this omits the server that would need to emit the template tags but it hopefully is enough to demonstrate a simple path is possible.

In fact this proof of concept was the inspiration for https://enhance.dev which takes this simple idea and takes it to a logical conclusion.

No build step using only web standards and can leverage single file components as HTML files.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment