Custom HTML element for importing HTML documents (or parts of documents) into other HTML documents on the fly! First install the package:
npm install @sirpepe/html-import
Then either load the module or the minified bundle and then use
<html-import src>
in your HTML:
<!-- Importing body content -->
<html-import src="content.html">
<p>
This text gets replaced when the content of the body element from
content.html once it has loaded. If the browser does not support custom
elements, this text is rendered as a fallback.
</p>
</html-import>
<!-- Importing only content that matches a specific selector -->
<html-import src="content.html" selector=".foo">
<p>
This text gets replaced by elements matching .foo (that are not nested
inside something matching .foo themselves) in content.html when it has
loaded. Note that the selector's scope is not limited to the body, so you
can import elements from a document's head as well.
</p>
</html-import>
<!-- Reactive import -->
<html-import src="">
<p>
This text gets replaced by whatever the src property is set to if and when
it gets set to something. This way the element works kindof like an iframe.
</p>
</html-import>
Notable features:
- Import whatever you like: plain HTML elements, style and link elements and even script elements work. Non-blocking scripts (script elements with
async
,defer
ortype="module"
) in imported HTML files work as expected. Blocking scripts will execute asynchronously, and thus may cause unintended effects. - Nest imports as much as needed (as long as no circular imports exist)
- Optionally filter imported elements by selector with an attribute:
<html-import src="a.html" selector=".foo"></html-import>
. Fragments also work:<html-import src="a.html#foo"></html-import>
. - Reactive imports: updating the
src
orselector
attributes replaces already imported content with new content as specified by the attributes - No frameworks, libraries or build tools required! You can use the ESM version of this component with your favorite build tool or just drop the minified version right into your web project.
- Easy to customize through subclassing, monkey patching or events handlers.
To use the element in HTML you have to import the main script somewhere. The
custom element will register itself automatically and upgrade any
already-existing instances of <html-import>
. Any new instances will initialize
in an upgraded state.
The element has two important HTML attributes:
src
defines the source HTML document for an import element. You can omit, remove or update the attribute at any time and the element will (if necessary) reload the source file and update itself. Both relative and absolute URLs work.selector
defines a selector for specific elements to import from the source document. Any CSS selector that your target browsers support is valid. You can omit, remove or update the attribute at any time and the element will (if necessary) reload the source file and update itself with the new elements. If theselector
attribute is missing or empty and thesrc
attribute is set, the import element will import all content from the target document's<body>
element (including text nodes). If theselector
attribute is set, it selects elements from the entire target document (not just the<body>
).
If the selector attribute is defined and if the src
attribute contains a URL
with a fragment identifier, the element will import only the fragment target
if it also matches the selector. In summary:
- no selector, no fragment in URL = entire body contents
- selector, no fragment in URL = all elements matching the selector
- no selector, fragment in URL = first element matching the fragment
- selector and fragment in URL = first element matching the fragment if it that also matches selector
The element performs fetch()
requests under the hood. Once such a request has
finished, the element's contents get replaced by whatever the result HTML
(optionally filtered by the selector). The content between an <html-import>
element's tags thus serves as both its initial content and its fallback content
in case some scripts break or an ancient browser without support for custom
elements comes along.
The JS API for <html-import>
consists of a constructor function, four events,
two method, and three DOM properties (plus attributes for event handlers).
You can construct instances of the element by using the HTMLImportElement
constructor:
import HTMLImportElement from "html-import";
let myImportElement = new HTMLImportElement(
"/optional/initial/src/value",
"#optionalSelector"
);
document.body.append(myImportElement);
If you don't use ESM modules, you can always get access to the constructor via the custom elements registry:
window.customElements.whenDefined("html-import").then(() => {
let HTMLImportElement = window.customElements.get("html-import");
});
HTMLImportElement
can fire four events:
importstart
: Fires when the element starts to load content, eg. after a change to the attributessrc
orselector
or after callingreload()
importdone
: Fires when the element has imported contentimportfail
Fires when importing content has failed (eg. due to 404). The event object implements a propertydetail
that contains the reason for the failure.importabort
Fires when the element was about to import content, but got interrupted (eg. by a newsrc
value) before it could finish
All four events bubble and aren't cancelable. Note that you can use old-school
attribute event handlers a la <html-import onimportdone="...">
in addition to
addEventListener()
.
HTMLImportElement
implements two DOM methods:
reload()
causes a re-load without the need to change any attributes or properties. The method takes no arguments and returns a promise that resolves to an array of imported elements and the imported document's titles (just like the promise returned bydone
as described below).done()
returns a promise for that resolves when the element's target document has been loaded (or has failed to load due to an error).
The promises returned by reload()
and done()
resolve to an array of data
about what was imported:
let element = document.querySelector("html-import");
element.done.then( (data) => {
// data[0] = { element: affectedImportElement; title: "Imported document's title"; }
// data[1] = { element: firstNestedImportElement; title: "Nested imported document's title"; }
// etc.
});
HTMLImportElement
implements eight DOM properties:
src
reflects thesrc
content attribute. Can be used as a setter to change thesrc
value. As a getter, it always returns absolute URLs, even when the HTML attribute is relative (just like a<a>
element'shref
attribute). Returns the empty string when there's nosrc
set.selector
reflects theselector
content attribute. Can be used as a setter to change theselector
value.state
returns one of"loading"
,"done"
,"fail"
and "ready"`, indicating the element's loading stateonstart
,ondone
,onfail
andonabort
event handlers
Note that the internal loading mechanism for <html-import>
batches attribute
updates and cleanly terminates any ongoing request if a change to src
or
selector
occurs. You don't have to concern yourself with efficiency, just set
whatever attributes or DOM properties you want to change!
Any change to the attributes src
or selector
causes the imported content to
update. The old nodes get removed and replaced by the newly imported content.
This means that it is easy to "ajaxify" any old collection of static HTML
documents:
- Just wrap every page's main content in
<html-import>
(a minimal change to your project's main template) - Add a bit of javascript to intercept navigation events and manage the history (you can use your favorite routing library or just write a few lines yourself)
<!DOCTYPE html>
<html lang="en">
<meta charset="utf-8">
<title>Static site - Start</title>
<script defer src="../../dist/html-import.min.js"></script>
<script defer src="./script.js"></script>
<h1>Demo site</h1>
<ul>
<li><a class="active" href="./">Start</a></li>
<li><a href="about.html">About</a></li>
<li><a href="contact.html">Contact</a></li>
</ul>
<html-import src="" selector="html-import > *">
<h2>Welcome to the demo's start page</h2>
</html-import>
Clicks on the navigation links update the <html-import>
element's src
attribute, which causes it to load the respective page, extract the content from
the <html-import>
element from there and dump it into this page's
<html-import>
element. Add a little extra JS for routing and your whole page
suddenly feels like a SPA - when all you needed to do was to wrap every page's
main content in <html-import>
and write about 50 lines of JavaScript to
intercept clicks and manage the navigation history. If something breaks or an
ancient browser comes along, your project will still work via traditional page
loads.
Check out demo/staticsite/index.html
to see this principle in action.
You can easily customize the element's behavior by subclassing or monkey
patching HTMLImportElement
. Three methods on the HTMLImportElement
class
are hooks for extensions:
public async fetch(url: string, signal: AbortSignal): Promise<string>
downloads the text content from a URLpublic beforeReplaceContent(content: DocumentFragment): DocumentFragment
modifies the content before it is usedreplaceContent(newContent: DocumentFragment): void
controls the actual replacing of the old content
The following HTMLImportMarkdownElement
illustrates how you can easily build
on top of <html-import>
:
import marked from "marked";
import HTMLImportElement from "html-import";
export default class HTMLImportMarkdownElement extends HTMLImportElement {
get [Symbol.toStringTag]() {
return "HTMLMarkdownImportElement";
}
beforeReplaceContent(content) {
const contentContainer = this.ownerDocument.createElement("template");
let html = "";
for (const node of content.childNodes) {
if (node.nodeType === Node.TEXT_NODE) {
html += node.textContent;
} else if (node instanceof HTMLElement) {
html += node.innerText;
}
}
contentContainer.innerHTML = marked(html);
return contentContainer.content;
}
}
window.customElements.define("markdown-import", HTMLImportMarkdownElement);
Instead of subclassing, you can always monkey patch the prototype. If, for
example, you'd like to place the imported content in a shadow tree, just
overwrite replaceContent()
like this:
HTMLImportElement.prototype.replaceContent = function(newContent) {
if (!this.shadowRoot) {
this.shadowRoot = this.attachShadow({ mode: "open" });
} else {
this.shadowRoot.innerHTML = "";
}
this.shadowRoot.append(newContent);
}
Try demo/monkeypatch/index.html
to see this hack in action!
To run code on newly imported content each time the content changes, you can
also add a listener to the importdone
event and modify the event's target
content (that is, the content that has just been inserted into the
<html-import>
element in question) as needed:
window.addEventListener("importdone", (evt) => doStuff(evt.target.children));
- 3.0.0: Remove
verbose
property, turndone
from a getter into a method, simplify rules for promise resolution, replaceCustomEvent
with proper event subclasses, remove OnEventMixin, switch license to MIT, update toolchain - 2.1.0: Add
verbose
property - 2.0.1: Fix a bug that prevented scripts that were nested in other elements from being imported properly
- 2.0.0: Complete rewrite
- Because I'm a lazy linux-using slob this element has so far only been tested in Chrome and Firefox on Ubuntu.
MIT