Author: @dandclark
This document is intended as a starting point for engaging the community and standards bodies in developing collaborative solutions fit for standardization. As the solutions to problems described in this document progress along the standards-track, we will retain this document as an archive and use this section to keep the community up-to-date with the most current standards venue and content location of future work and discussions.
- This document status: ARCHIVED
- Current venue: Web Components Incubator of the W3C Web Applications Working Group and WHATWG HTML and DOM Workstreams | w3c/webcomponents |
- Current version: HTML Modules Explainer
We are proposing an extension of the ES6 Script Modules system to include HTML Modules. These will allow web developers to package and access declarative content from script in a way that allows for good componentization and reusability, and integrates well into the existing ES6 Modules infrastructure.
The introduction of ES6 Script Modules has provided several benefits for JavaScript developers including more componentized code and better dependency management. However, easy access to declarative content has been a consistent limitation with Script Modules. For example, if one wants to pack a custom element definition in a module, how should the HTML for the element's shadow tree be created? Current solutions would involve generating it dynamically (document.createElement, innerHTML or by using template literals), but it would be preferable from both a developer convenience and from a performance standpoint to simply write HTML and include it with the module. With HTML Modules this will be possible.
There is clear demand for this functionality in the developer community -- see this thread where ideas pertaining to HTML Modules have resulted in a great deal of developer and browser implementer engagement.
HTML Imports were proposed (and implemented in Chromium) as a solution, but they were developed independently of ES6 Modules and have several limitations:
- Global object pollution: vars created in an HTML Import show up on the global object by default. An ideal solution would minimize such side-effects. Accordingly, global object pollution does not occur in ES6 Modules.
- Parse blocking with inline script: the parsing of an HTML Import will block the main document's parser if included prior to an inline script element. ES6 Modules have defer semantics and thus do not block the parser.
- Independent dependency resolution infrastructures between HTML Imports and ES6 Script Modules: since these systems were developed independently their infrastructures for dependency resolution don't talk to each other, leading to missed performance opportunities and to bugs like this one.
- Non-intuitive import pass through: HTML Imports require the consumer to access their content from standard DOM queries like getElementById and querySelector. This is clumsy and limited relative to Script Modules' import/export statements that allow for explicit specification of the API surface provided by a module.
Integrating HTML Modules into the existing ES6 Module system, rather than creating it as a standalone component, will address these gaps.
HTML Modules will be imported using the same import
statements currently used for Script Modules:
<script type="module">
import {content} from "import.html"
document.body.appendChild(content);
</script>
The MIME-type in the HTTP response header will be checked to determine whether a given module should be treated as script or HTML. Each imported HTML Module will have its own module record as introduced in the ES6 spec and will participate in the ES6 Module map and module dependency graphs.
An HTML Module will be parsed per the normal HTML5 parsing rules, with the exception that it is only allowed to contain <script>
elements of type="module"
(non-module scripts will cause HTML Module creation to fail). This greatly simplifies the integration of HTML Modules into the current ES6 Module system since module scripts have defer semantics and we therefore don't need to worry about synchronous script elements causing side-effects during parsing. This allows us to resolve the entire import graph before executing any script -- which is a key aspect of the ES6 Modules system.
HTML Modules operate in the same JavaScript realm of the window in which they were imported, and the document
keyword in an HTML Module's inline script refers to the top-level document (as is the case in Script Modules today). In order for script in an HTML Module to access the module's own HTML content, we introduce a new import.meta.document
property that refers to the HTML Module document. We limit this to inline module scripts only because a non-inline module can be imported and run from multiple contexts, making its referrer document ambiguous. Inline scripts are unique to the document into which they are inlined, thus avoiding this problem.
The following example shows how an HTML Module might use import.meta.document
to access its HTML content and use it to define the shadow DOM for a custom element:
module.html
<template id="myCustomElementTemplate">
<div>Custom element shadow tree content...</div>
</template>
<script type="module">
let importDoc = import.meta.document;
class myCustomElement extends HTMLElement {
constructor() {
super();
let shadowRoot = this.attachShadow({ mode: "open" });
let template = importDoc.getElementById("myCustomElementTemplate");
shadowRoot.appendChild(template.content.cloneNode(true));
}
}
window.customElements.define("myCustomElement", myCustomElement);
</script>
An HTML Module will specify its exports using its inline script elements. Inline Script Modules in an HTML Module document will have their exports redirected via the HTML Module Record such that the importer of the HTML Module can access them. This is done by computing the exports for the HTML Module's record during its instantiation phase as per the following pseudocode:
for (ModuleScript in HtmlModuleRecord.[[RequestedModules]]) {
if (ModuleScript.IsFromInlineScriptElement) {
export * from ModuleScript;
}
}
This is the fundamental way in which HTML Modules will expose their content to their referrer (the document/module that imported them) -- the HTML Module can take its HTML elements, classes, functions, etc., and export them from any inline script in the HTML Module's document. This allows HTML Modules to operate with the same import/export syntax as ES modules. Note that in the existing ES6 implementation, exports from an inline Script Module previously had no real use; in this way we grant them one.
Example:
module.html
<div id="blogPost">
<p>Content...</p>
</div>
<script type="module">
let blogPost = import.meta.document.querySelector("#blogPost");
export {blogPost}
</script>
blog.html
<script type="module">
import {blogPost} from "module.html"
document.body.appendChild(blogPost);
</script>
Additionally, if an inline script element specifies a default export it will be passed through as the default export of the HTML Module (multiple inline scripts specifying a default will result in an instantiation error for the HTML Module). If no script specifies a default export, the HTML Module's document will be the implicit default export of the module. This will allow declarative content of a module to be consumed without the use of inline script elements in the module to specify exports.
Example:
module.html
<template id="myCustomElementTemplate"></template>
main.html:
import importedDoc from "module.html"
let theTemplate = importedDoc.querySelector("template");
The following example demonstrates how a custom element definition could be packaged as an HTML Module:
This example demonstrates how the above custom element definition can be consumed: