├── 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 | --------------------------------------------------------------------------------