');
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 `