├── server
├── .eslintrc.json
├── start.js
└── index.js
├── hello-world.html
├── app.js
├── .eslintrc.json
├── package.json
├── index.html
├── hello-world.js
├── sw.js
└── README.md
/server/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": false,
4 | "node": true
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/hello-world.html:
--------------------------------------------------------------------------------
1 |
Hello, world!
2 | Coming at you live, from an HTML Template imported as an ES Module!
3 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | // Register the Service Worker which will polyfill the HTML module behavior.
2 | navigator.serviceWorker.register('./sw.js');
3 |
4 | // Unfortunately, we have to wait for the Service Worker to ready before
5 | // actually loading the application so that it can intercept the HTML requests.
6 | navigator.serviceWorker.ready.then(() => import('./hello-world.js'));
7 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb-base",
3 | "env": {
4 | "browser": true
5 | },
6 | "rules": {
7 | "import/no-extraneous-dependencies": "off",
8 | "max-len": "off",
9 | "no-plusplus": "off",
10 | "no-restricted-globals": "off",
11 | "no-use-before-define": "off",
12 | "padded-blocks": ["error", "always"],
13 | "prefer-destructuring": "off"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/server/start.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This is a simple server for use in development. It is NOT used in production.
3 | */
4 |
5 | const path = require('path');
6 | const express = require('express');
7 |
8 | const server = express();
9 |
10 | const root = path.resolve(__dirname, '..');
11 |
12 | server.use(express.static(root));
13 |
14 | module.exports = function startServer(port) {
15 |
16 | return new Promise((resolve) => {
17 |
18 | const listener = server.listen(port, () => resolve(listener));
19 |
20 | });
21 |
22 | };
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "html-template-es-modules",
3 | "version": "1.0.0",
4 | "description": "A simple, proof-of-concept for importing HTML Templates into ES Modules",
5 | "scripts": {
6 | "start": "node ./server",
7 | "lint": "eslint '**/*.js'"
8 | },
9 | "author": "Trent M. Willis ",
10 | "license": "MIT",
11 | "devDependencies": {
12 | "eslint": "^4.16.0",
13 | "eslint-config-airbnb-base": "^12.1.0",
14 | "eslint-plugin-import": "^2.8.0",
15 | "express": "^4.16.2",
16 | "opn": "^5.3.0",
17 | "ora": "^1.3.0"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | HTML Template ES Modules
9 |
10 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/hello-world.js:
--------------------------------------------------------------------------------
1 | import template from './hello-world.html';
2 |
3 | class HelloWorldElement extends HTMLElement {
4 |
5 | // Defines the name of the component
6 | static get is() {
7 |
8 | return 'hello-world';
9 |
10 | }
11 |
12 | constructor() {
13 |
14 | super();
15 |
16 | // Add shadow dom
17 | this.attachShadow({ mode: 'open' });
18 |
19 | // Append a clone of the template
20 | this.shadowRoot.appendChild(template.content.cloneNode(true));
21 |
22 | }
23 |
24 | }
25 |
26 | // Register the component as a custom element
27 | customElements.define(HelloWorldElement.is, HelloWorldElement);
28 |
29 | export default HelloWorldElement;
30 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This is a simple server for use in development. It is NOT used in production.
3 | */
4 |
5 | const opn = require('opn');
6 | const ora = require('ora');
7 | const startServer = require('./start');
8 |
9 | const port = 8080;
10 |
11 | const colorReset = '\x1b[0m';
12 | const blue = '\x1b[34m';
13 | const green = '\x1b[32m';
14 |
15 | function colorize(color, string) {
16 |
17 | return `${color}${string}${colorReset}`;
18 |
19 | }
20 |
21 | const spinner = ora('Starting development server').start();
22 | startServer(port).then(() => {
23 |
24 | spinner.succeed('Development server ready');
25 |
26 | /* eslint-disable no-console */
27 | console.log();
28 | console.info(`${colorize(blue, 'ℹ')} View the ${colorize(blue, 'application')} at ${colorize(green, `http://localhost:${port}`)}`);
29 | console.log();
30 | /* eslint-enable no-console */
31 |
32 | opn(`http://localhost:${port}`);
33 |
34 | });
35 |
--------------------------------------------------------------------------------
/sw.js:
--------------------------------------------------------------------------------
1 | // Call clients.claim so that we intercept requests even on initial page load.
2 | self.addEventListener('activate', () => self.clients.claim());
3 |
4 | self.addEventListener('fetch', (event) => {
5 |
6 | if (isTemplateRequest(event.request)) {
7 |
8 | event.respondWith(esModuleFromTemplateRequest(event.request));
9 |
10 | }
11 |
12 | });
13 |
14 | function isTemplateRequest(request) {
15 |
16 | return request.url.endsWith('.html') && request.destination === 'script';
17 |
18 | }
19 |
20 | async function esModuleFromTemplateRequest(request) {
21 |
22 | const response = await fetch(request); // Fetch the original data
23 | const markup = await response.text(); // Get the raw markup as text
24 | const esModule = insertMarkupIntoESModule(markup);
25 | const responseOptions = { // Return the new "module" as the appropriate type
26 | headers: {
27 | 'Content-Type': 'application/javascript',
28 | },
29 | };
30 |
31 | return new Response(esModule, responseOptions);
32 |
33 | }
34 |
35 | function insertMarkupIntoESModule(html) {
36 |
37 | return `
38 | const template = document.createElement('template');
39 | template.innerHTML = \`${html}\`;
40 | export default template;
41 | `;
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # HTML Template ES Modules
2 |
3 | A simple, proof-of-concept for importing HTML files as HTML Templates into ES Modules. Made possible by using [`Service Workers`](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) to transparently transform HTML file responses into ES Modules that export an [`HTMLTemplateElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTemplateElement).
4 |
5 | ## Example
6 |
7 | Given [`hello-world.html`](./hello-world.html):
8 |
9 | ```html
10 | Hello, world!
11 | Coming at you live, from an HTML Template imported as an ES Module!
12 | ```
13 |
14 | You can import it in [`hello-world.js`](./hello-world.js) like so:
15 |
16 | ```js
17 | import template from './hello-world.html';
18 | console.log(template.toString()); // [object HTMLTemplateElement]
19 | ```
20 |
21 | From there you can clone the template and insert it into the DOM! Or do whatever else you'd like with the element.
22 |
23 | ## Running The Demo
24 |
25 | To try out the demo, do the following:
26 |
27 | * Clone the repo: `git clone https://github.com/trentmwillis/html-template-es-modules.git`
28 | * Install dependencies in the repo: `cd html-template-es-modules && npm install`
29 | * Run the server: `npm run start`
30 |
--------------------------------------------------------------------------------