├── .gitignore ├── test ├── test-1.html ├── styles.css ├── test-styles.html ├── index.html └── html-include-element_test.js ├── web-test-runner.config.js ├── package.json ├── CHANGELOG.md ├── html-include-element.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /test/test-1.html: -------------------------------------------------------------------------------- 1 |

TEST

2 | -------------------------------------------------------------------------------- /test/styles.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/test-styles.html: -------------------------------------------------------------------------------- 1 | 2 |

TEST

3 | -------------------------------------------------------------------------------- /web-test-runner.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | testFramework: { 3 | config: { 4 | ui: 'tdd', 5 | timeout: '2000', 6 | }, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-include-element", 3 | "version": "0.3.0", 4 | "description": "Include HTML files into your page", 5 | "type": "module", 6 | "main": "html-include-element.js", 7 | "scripts": { 8 | "test": "wtr test/**/*_test.js --node-resolve" 9 | }, 10 | "author": "Justin Fagnani ", 11 | "license": "Apache-2.0", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/justinfagnani/html-include-element.git" 15 | }, 16 | "files": [ 17 | "html-include-element.js" 18 | ], 19 | "devDependencies": { 20 | "@esm-bundle/chai": "^4.3.4-fix.0", 21 | "@web/test-runner": "^0.13.27" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | 12 | 13 | ## Unreleased 14 | 15 | 16 | 17 | 18 | 19 | 20 | ## [0.3.0] - 2022-03-23 21 | 22 | ### Added 23 | 24 | * Added `delegates-focus` attribute: https://github.com/justinfagnani/html-include-element/pull/13 25 | 26 | ### Fixed 27 | 28 | * Fixed a bug with the `load` event: https://github.com/justinfagnani/html-include-element/pull/12 29 | 30 | 31 | ## [0.2.0] - 2020-02-21 32 | 33 | ### Added 34 | * Defer firing the `load` event until all `` elements in the shadow root are finished loading. 35 | ### Fixed 36 | * Preserve light DOM content when including into shadow DOM 37 | 38 | ## [0.1.3] - 2019-05-01 39 | 40 | ### Fixed 41 | 42 | * Add `` to project content when using `no-shadow` 43 | 44 | ## [0.1.2] - 2019-04-16 45 | 46 | ### Fixed 47 | * Handle possible race condition when changing the `src` attribute. 48 | 49 | ## [0.1.1] - 2019-04-16 50 | 51 | ### Fixed 52 | * Add repository to package.json 53 | 54 | ## [0.1.0] - 2019-04-16 55 | 56 | ### Initial Release 57 | -------------------------------------------------------------------------------- /test/html-include-element_test.js: -------------------------------------------------------------------------------- 1 | import {assert} from '@esm-bundle/chai'; 2 | 3 | import '../html-include-element.js'; 4 | 5 | suite('html-include-element', () => { 6 | 7 | let container; 8 | 9 | setup(() => { 10 | container = document.createElement('div'); 11 | document.body.appendChild(container); 12 | }); 13 | 14 | teardown(() => { 15 | document.body.removeChild(container); 16 | container = undefined; 17 | }); 18 | 19 | test('includes some HTML', async () => { 20 | container.innerHTML = ` 21 | 22 | `; 23 | const include = container.querySelector('html-include'); 24 | await new Promise((res) => { 25 | include.addEventListener('load', () => res()); 26 | }); 27 | assert.equal(include.shadowRoot.children[0].tagName, 'STYLE'); 28 | assert.equal(include.shadowRoot.children[1].outerHTML, '

TEST

'); 29 | }); 30 | 31 | test('includes some HTML in light DOM', async () => { 32 | container.innerHTML = ` 33 | 34 | `; 35 | const include = container.querySelector('html-include'); 36 | await new Promise((res) => { 37 | include.addEventListener('load', () => res()); 38 | }); 39 | assert.equal(include.innerHTML.trim(), '

TEST

'); 40 | }); 41 | 42 | test('preserves light DOM when including to shadow DOM', async () => { 43 | container.innerHTML = ` 44 | TEST 45 | `; 46 | const include = container.querySelector('html-include'); 47 | await new Promise((res) => { 48 | include.addEventListener('load', () => res()); 49 | }); 50 | assert.equal(include.innerHTML, 'TEST'); 51 | }); 52 | 53 | test('waits for styles to load', async () => { 54 | container.innerHTML = ` 55 | TEST 56 | `; 57 | const include = container.querySelector('html-include'); 58 | await new Promise((res) => { 59 | include.addEventListener('load', () => { 60 | assert.isNotNull(include.shadowRoot.querySelector('link').sheet); 61 | res(); 62 | }); 63 | }); 64 | }); 65 | 66 | // TODO: tests for mode & changing src attribute 67 | 68 | }); 69 | -------------------------------------------------------------------------------- /html-include-element.js: -------------------------------------------------------------------------------- 1 | const LINK_LOAD_SUPPORTED = 'onload' in HTMLLinkElement.prototype; 2 | 3 | /** 4 | * Firefox may throw an error when accessing a not-yet-loaded cssRules property. 5 | * @param {HTMLLinkElement} 6 | * @return {boolean} 7 | */ 8 | function isLinkAlreadyLoaded(link) { 9 | try { 10 | return !!(link.sheet && link.sheet.cssRules); 11 | } catch (error) { 12 | if (error.name === 'InvalidAccessError' || error.name === 'SecurityError') 13 | return false; 14 | else 15 | throw error; 16 | } 17 | } 18 | 19 | /** 20 | * Resolves when a `` element has loaded its resource. 21 | * Gracefully degrades for browsers that don't support the `load` event on links. 22 | * in which case, it immediately resolves, causing a FOUC, but displaying content. 23 | * resolves immediately if the stylesheet has already been loaded. 24 | * @param {HTMLLinkElement} link 25 | * @return {Promise} 26 | */ 27 | async function linkLoaded(link) { 28 | return new Promise((resolve, reject) => { 29 | if (!LINK_LOAD_SUPPORTED) resolve(); 30 | else if (isLinkAlreadyLoaded(link)) resolve(link.sheet); 31 | else { 32 | link.addEventListener('load', () => resolve(link.sheet), { once: true }); 33 | link.addEventListener('error', reject, { once: true }); 34 | } 35 | }); 36 | } 37 | 38 | /** 39 | * Embeds HTML into a document. 40 | * 41 | * The HTML is fetched from the URL contained in the `src` attribute, using the 42 | * fetch() API. A 'load' event is fired when the HTML is updated. 43 | * 44 | * The request is made using CORS by default. This can be chaned with the `mode` 45 | * attribute. 46 | * 47 | * By default, the HTML is embedded into a shadow root. If the `no-shadow` 48 | * attribute is present, the HTML will be embedded into the child content. 49 | * 50 | */ 51 | export class HTMLIncludeElement extends HTMLElement { 52 | static get observedAttributes() { 53 | return ['src', 'mode', 'no-shadow']; 54 | } 55 | 56 | /** 57 | * The URL to fetch an HTML document from. 58 | * 59 | * Setting this property causes a fetch the HTML from the URL. 60 | */ 61 | get src() { 62 | return this.getAttribute('src'); 63 | } 64 | 65 | set src(value) { 66 | this.setAttribute('src', value); 67 | } 68 | 69 | /** 70 | * The fetch mode to use: "cors", "no-cors", or "same-origin". 71 | * See the fetch() documents for more information. 72 | * 73 | * Setting this property does not re-fetch the HTML. 74 | */ 75 | get mode() { 76 | return this.getAttribute('mode'); 77 | } 78 | 79 | set mode(value) { 80 | this.setAttribute('mode', value); 81 | } 82 | 83 | /** 84 | * If true, replaces the innerHTML of this element with the text response 85 | * fetch. Setting this property does not re-fetch the HTML. 86 | */ 87 | get noShadow() { 88 | return this.hasAttribute('no-shadow'); 89 | } 90 | 91 | set noShadow(value) { 92 | if (!!value) { 93 | this.setAttribute('no-shadow', ''); 94 | } else { 95 | this.removeAttribute('no-shadow'); 96 | } 97 | } 98 | 99 | constructor() { 100 | super(); 101 | this.attachShadow({mode: 'open', delegatesFocus: this.hasAttribute('delegates-focus')}); 102 | this.shadowRoot.innerHTML = ` 103 | 108 | `; 109 | } 110 | 111 | async attributeChangedCallback(name, oldValue, newValue) { 112 | if (name === 'src') { 113 | let text = ''; 114 | try { 115 | const mode = this.mode || 'cors'; 116 | const response = await fetch(newValue, {mode}); 117 | if (!response.ok) { 118 | throw new Error(`html-include fetch failed: ${response.statusText}`); 119 | } 120 | text = await response.text(); 121 | if (this.src !== newValue) { 122 | // the src attribute was changed before we got the response, so bail 123 | return; 124 | } 125 | } catch(e) { 126 | console.error(e); 127 | } 128 | // Don't destroy the light DOM if we're using shadow DOM, so that slotted content is respected 129 | if (this.noShadow) this.innerHTML = text; 130 | this.shadowRoot.innerHTML = ` 131 | 136 | ${this.noShadow ? '' : text} 137 | `; 138 | 139 | // If we're not using shadow DOM, then the consuming root 140 | // is responsible to load its own resources 141 | if (!this.noShadow) { 142 | await Promise.all([...this.shadowRoot.querySelectorAll('link')].map(linkLoaded)); 143 | } 144 | 145 | this.dispatchEvent(new Event('load')); 146 | } 147 | } 148 | } 149 | customElements.define('html-include', HTMLIncludeElement); 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `` 2 | 3 | Easily include external HTML into your pages. 4 | 5 | ## Overview 6 | 7 | `` is a web component that fetches HTML and includes it into your page. 8 | 9 | ```html 10 | 11 | ``` 12 | 13 | `` works with any framework, or no framework at all. 14 | 15 | By default `` renders the HTML in a shadow root, so it's isolated from the rest of the page. This can be configured with the `no-shadow` attribute. 16 | 17 | ## Installation 18 | 19 | Install from npm: 20 | 21 | ```bash 22 | npm i html-include-element 23 | ``` 24 | 25 | Or load from a CDN like unpkg.com: `https://unpkg.com/html-include-element` 26 | 27 | ## Usage 28 | 29 | `` is distributed as standard JS modules, which are supported in all current major browsers. 30 | 31 | You can load it into a page with a ` 36 | 37 | 38 | 39 | 40 | ``` 41 | 42 | Or import into a JavaScript module: 43 | 44 | ```js 45 | import {HTMLIncludeElement} from 'html-include-element'; 46 | ``` 47 | 48 | `` fires a `load` even when the included file has been loaded. When including into shadow DOM (the default behavior) the `load` event is fired after any `` elements in the included file have loaded as well. 49 | 50 | This allows you to hide the `` element and show it after the `load` event fires to avoid flashes of unstyled content. 51 | 52 | ### Same-origin policy and CORS 53 | 54 | `` uses the `fetch()` API to load the HTML. This means it uses the same-origin security model and supports CORS. In order to load an external resource it must either be from the same origin as the page, or send CORS headers. You can control the fetch mode with the `mode` attribute. 55 | 56 | ### Styling included HTML 57 | 58 | When included into shadow DOM, the HTML and its styles are isolated from the rest of the page. Main page selectors will not select into the content, and the included HTML can have `