importAs() and HTML Modules
This repo contains a proof-of-concept of the "import as" or "generic imports" proposal. See this comment thread for some background and context.
View a live demo with real running code at ashleyscirra.github.io/import-as-and-html-modules/index.html. Note this requires a browser with Modules support, dynamic
import.meta. All the code is in this repo so you can see how it is implemented.
The intent of this proposal is to ultimately allow something like this:
import doc from './my-html-module.html' as HTMLModule;
There are a few tricky aspects of supporting the above syntax. In particular
import statements are designed to be statically analysable, but calling in to a library function like
HTMLModule requires adding dynamic features to imports. To avoid having to solve this, the proof-of-concept is implemented only in the style of dynamic import(). To avoid conflicting with
import(), the proof-of-concept uses a method named
importAs(), but the intent is that these features could work with
import() too, and ultimately static imports as well.
How it works
The feature is very simple. It starts with a call to:
This will call
Type[Symbol.importer](url) which returns a Promise. There could be built-in importers for e.g.
Symbol.importer. This provides a hook to implement HTML Modules entirely by a library.
importAs(url) with no type will fall back to
You can use the import system to load static resources. This means the import system can be used to load all your resources, in whichever format you want them, rather than being limited to other JS modules only. Examples:
const str = await importAs("file.txt", String); const json = await importAs("file.json", JSON); const blob = await importAs("file.dat", Blob); const arrayBuffer = await importAs("file.dat", ArrayBuffer); const imageElem = await importAs("file.png", HTMLImageElement); const imageBitmap = await importAs("file.png", ImageBitmap); const imageData = await importAs("file.png", ImageData);
Many of these will work in a Worker too, but the DOM-based ones (e.g. HTMLImageElement) can't.
You can also load static documents to access other snippets of DOM. The library-implemented
SelectorAll importers make this convenient to use.
const doc = await importAs("file.html", Document); const templateElem = await importAs("file.html", Selector("#templateElem")); const itemElems = await importAs("file.html", SelectorAll(".item"));
This is intended to take advantage of existing de-duplication mechanisms in the module system. In the above example, only one request to
file.html ought to be made, since the subsequent requests will be de-duplicated and return the same resource. This makes it efficient to load a variety of elements from the same document with the
MainDocumentStyle also allows applying main document styles, like HTML imports used to do. It shows how the feature can be easily adapted to cover styling too, which is also used in the HTML Modules implementation.
await importAs("file.css", MainDocumentStyle); // style now applied
This repo provides a proof-of-concept
HTMLModule importer which allows:
const htmlModule = await importAs("file.html", HTMLModule); htmlModule.document; // document representing file.html, e.g. to get elements htmlModule.exports; // Map of tag -> default export for script modules in file.html
HTML Modules are implemented similarly to HTML imports. The implementation does the following:
- loads "file.html" as a document
- looks for
<link rel="stylesheet">elements and applies them to the main document style
- looks for
- classic scripts are simply loaded and executed
- module scripts are loaded with
import(), and their default export added to
- provides a
getCurrentHtmlModule()global function for scripts to identify their associated HTML module, allowing them to access DOM elements from its document, as well as looking up other script's exports
- looks for a made-up
<link rel="html-module">which represents a sub-module, allowing for nested HTML module dependencies; nested module exports are merged in to
- resolves the promise when all sub-resources have finished loading
- The dialog markup, style and script are all entirely self-contained, making it an independent module.
- The dialog script is a module. It exports its dialog class as a default export, making it accessible to the caller via
- The script can access DOM content with
getCurrentHtmlModule(), so it can find its
<dialog>tag and call
showModal()on it. There are many other use cases here, particularly loading a
- The style is applied to the main document, so it affects the dialog when it is displayed. The style is of course global, but it is very easy to scope all its styles to the dialog by using its ID selector. This helps make CSS maintainable.
- It includes a sub-dialog it depends on using
<link rel="html-module" href="dialog2/dialog2.html">. Since order-of-execution is guaranteed, the first dialog can safely use
Dialog2.Show(), defined in the sub-module.
- No static import syntax
- The HTMLModule implementation loads sequentially, since it's just a proof-of-concept. Obviously a production library would parallelise loading for maximum performance. Additionally real-world apps would likely concatenate HTML Modules down in to fewer resources, much as vulcanize does for HTML imports.
An alternative approach
The dialog could also be imported as a normal JS module, and load its DOM and style like this:
Promise.all([ importAs("dialog1.html", Selector("#dialog1")), importAs("dialog1.css", MainDocumentStyle) ])
or if the static import is supported:
import dialogElem from "./dialog1.html" as Selector("#dialog1"); import "./dialog1.css" as MainDocumentStyle;
This means the entire dependency tree stays in script modules, and each module pulls in the additional resources it needs. Basically it makes script fetch HTML and CSS, rather than having HTML fetch scripts, CSS and more HTML. Still, if this syntax is supported, it means HTML modules can be implemented entirely as a JS library. Then there's no need to come up with a specification for HTML modules. They can both be done under this same proposal and it's up to the developer what they do with it.