Open EisenbergEffect opened 6 months ago
I tend to think that importing and doing things with the imports should be separated. Then you have a general mechanism for defining and consuming identifiers, and many features can plug into either side of that.
So I'd prefer to have an import just define a identifiers*, and something like a scoped custom element registry just use those identifiers. I don't think an import should also define a custom elements - that seems too coupled.
An import could allow you to import one or more identifiers from the module, and define a identifiers in some HTML scope:
<import src="./element-one.html" imports="element-one element-two"></import>
Because we want an equivalence between JS and HTML modules, this should work with JS too:
<import src="./element-one.js" type="js" imports="element-one element-two"></import>
I think this might be a reason to not use fragments in the imports. Though maybe when you import from JS, the fragments just reference exports?
Note that type="js"
is similar to import attributes, but the default is flipped, because we might want HTML modules to be the default for <import>
.
Another option is to just use the existing <script>
tag, but allow it to import identifiers:
<script src="./element-one.html" type="html" imports="element-one"></script>
If we want to meet the use case of running with JavaScript disabled, could we say that type="html"
is allowed if it doesn't contain or import JS itself?
We want HTML modules to be able to be be bundled and to just naturally be able to contain multiple component definitions. So we don't want to have to import an identifier, but also be able to declare them locally:
<!-- Defines, but does not register, a custom element class -->
<define id="element-one">...</define>
Note: I'm using the id
attribute here. Can we just reuse IDs for identifiers? It'd be simpler conceptually
Once we have a way to import or declare identifiers, we have unlocked HTML use-cases for non-side-effectful HTML modules. We can import a definition and register it ourselves:
<script src="./element-one.html" type="html" imports="element-one"></script>
<define id="element-two">
<registry>
<define tagname="el-one" from="element-one">
</registry>
<template>
<el-one></el-one>
</template>
</define>
Note I'm overloading <define>
here to define an element globally when it's not a child of a <registry>
and to define an element inside a scoped registry when it is. The example also implies that a declarative custom element definition just implicitly uses the first <registry>
child. We would need a way to reference shared registries too.
We could also just import templates from a separate file:
<script src="./template.html" type="html" imports="template-one"></script>
<define id="element-one">
<template from="template-one"></template>
</define>
Or maybe:
<script src="./template.html" type="html" imports="template-one"></script>
<define id="element-one" template="template-one">
</define>
@EisenbergEffect For script tag(s) inside of a module, are you still thinking it could utilize the import.meta.document
idea once proposed in the past incarnation of HTML Modules? Or would there be some other method of getting at the document or fragments?
@jaredcwhite Yes, I was thinking we could still use that in script tags within the module.
@EisenbergEffect
import {
pricingCard,
sharedHeaderStyles,
sharedHeader
} from "./my-resources.html" with { type: "html" };
You are importing a CSSStyleSheet from sharedHeaderStyles as type: “html”, which seems weird.
@o-t-w I agree. It's a bit of a quandry. While it would be more consistent for that to be a style element, what would be practically useful is a CSSStyleSheet.
I disagree about decoupling of import and registering in the scope. It creates extra unused namespace and problems with name scope negotiation and recognizing by develloper.
All languages (try to find otherwise) including JS have import and mapping to name in the same statement.
The following convention would fix this irrationality:
<define id="element-two">
<script src="./element-one.html" type="html" imports="element-one"></script>
<define tagname="el-one" from="element-one">
<template>
<el-one></el-one>
</template>
</define>
Not in favor of tag names though. It overlaps with standard ones (XSLT) wthout inheriting its semantics and syntax.
I could see enabling decoupled import and define but I also would prefer to be able to do it in one go. That seems like it would be the most common need:
<import src="./some-module.html#MyElement" tag="my-element">
<my-element>Hello</my-element>
<my-element>World</my-element>
I think I would also want to be able to import and use a template in one go as well.
<template src="./some-module.html#myTemplate">
Fallback content while the template is being loaded.
</template>
Could we get this working for declarative shadow dom too?
<template id="helloTemplate">Hello <slot></slot>!</template>
<say-hello>
<template shadowrootmode="open" src="#helloTemplate"></template>
World
</say-hello>
<say-hello>
<template shadowrootmode="open" src="#helloTemplate"></template>
Web Components
</say-hello>
and problems with name scope negotiation and recognizing by develloper
what problems, exactly?
The reason we should separate importing a thing from registering a custom element is that they will necessarily be different namespaces. From an HTML module we could import a DOM node, say a template or a reference to a custom element, and we need a way to refer to those as well as potentially register custom elements. If the import statement registers elements, it'll need two features - one for importing an identifier, another for registering custom elements. This is too coupled. We'd still need a way to register elements by HTML reference that aren't from an import - like a local definition.
I think this might be a reason to not use fragments in the imports. Though maybe when you import from JS, the fragments just reference exports?
Fragments already have existing behaviour with JS imports, in particular they trigger multiple evaluations (without triggering multiple fetches compared to using query params):
// mod.js
console.log(import.meta.url);
await import("./mod.js#one"); // Prints BASEURL/mod.js#one
await import("./mod.js#two"); // Prints BASEURL/mod.js#two
I think it would be preferable to have consistent behaviour rather than special casing import behaviour inside HTML. It'd probably be best if #
triggered individual module evaluations for HTML modules well.
One problem that needs to be resolved is how to handle import attributes, in JS import attributes are required to import non-JS things like CSS. There's not really anyway to avoid these in HTML either as they define headers and the request type for the associated fetch.
It does seem cumbersome to have to specify with attributes in many cases though, if we import per element kind as suggested by @justinfagnani then we could have defaults:
<!-- default behaviour imports the default import with attributes { type: "css" }-->
<style src="./path-to-css-module.css"></style>
<!-- importname and type can be customized with attributes -->
<style src"./path-to-js-module.js" import="cssSheet" importtype="js"></style>
<style src="./path-to-html-module.html" import="cssSheet" importtype="html"></style>
Why this over document[id]
or document.querySelector('#theID')
?
Would it be better to initially focus on standardising how HTML modules work with import attributes in JavaScript before adding new syntax?
Also, I was re-reading the original GitHub issue for HTML modules and this reply seemed worth pondering as an alternative to separate using separate Id and export. https://github.com/WICG/webcomponents/issues/645#issuecomment-427447824
Something that still needs to be considered for HTML modules is how to manage base urls for templates in HTML modules.
For example if you're using a template from an HTML module to make a custom element like:
<!-- template.html -->
<template export="default">
<link rel="stylesheet" href="./styles.css" />
<div id="someThing"></div>
</template>
import elTemplate from "./template.html";
class MyElement extends HTMLElement {
#shadowRoot = this.#attachShadow({ mode: "closed" });
constructor() {
super();
// OOPS the links are relative to the CURRENT document not to template.html
this.#shadowRoot.append(elTemplate.content.cloneNode(true));
}
}
then any links in your associated templates will refer to the current document not the document containing those templates.
Using a template or fragment is different. I propose adding a new element to handle this, named
compose
. It would look something like this:<compose src="#sharedHeader"></compose> <compose src="#productCard"></compose>
Note: We could enable the
src
attribute to both import and compose as a convenience. But I think importing and using are two different things and minimally need what is shown above.Open Question: Could
compose
be a processing instruction or something else besides an element? Ideally, the composition should not affect the dom structure. It's really only a location where the composition should occur.
Could <slot>
be reused for this? Something like this but with the standard <slot>
?
Background
Extensive discussion on Declarative Custom Elements has brought the CG to a point of realizing that a good starting point would be HTML Modules. HTML Modules would serve as a container for DCEs, but also other reusable HTML resources, such as templates.
This being the case, we need to focus in on exactly how exports AND imports would work. While the export side of HTML Modules has been explored previously, the import side is largely undefined.
Strawman
In order to get conversation going, I'll propose a few basic ideas here.
HTML Module Exports
I'll begin by submitting that there are five possible export types:
To export any one of these, they must have an
id
, which will become the symbol under which the export is provided, and they must have theexport
attribute. Elements with only anid
can be referenced within the module, but are not able to be imported. Bothid
andexport
are required for that.Here are a few examples.
The above types exist today, but in the future we could also add
<element>
(or some other tag) for declarative custom elements and<registry>
for a declarative custom element registry.Importing HTML Modules in JS
Given the above example, we could import in JavaScript, using import assertions.
You should not be able to import the entire document, as that would break the encapsulation of the module. You should only be able to import elements with an
id
and anexport
attribute. We could also enable an element with anexport
and noid
to be imported as the default export.Importing HTML Modules in HTML
Importing into HTML involves several tasks:
HTML already provides a way to declaratively use a custom element, but not a way to declaratively use a template or fragment. So, we'll need to add something new here.
We also have no way to declaratively define a custom element in a given HTML document or module.
First, let's tackle the issue of importing the resources from an HTML module. We could do that with an
<import>
element as such. Here are a few examples:The
from
attribute provides the path to the HTML module and also includes a fragment identifying the specific export. Theas
attribute provides a way to name the import within the current document or module. In the case of a template or fragment, this creates something with anid
denoted byas
. In the case of a custom element, it will calldefine
with the value ofas
for the tag name.To use an imported custom element, is no different than using any custom element:
Using a template or fragment is different. I propose adding a new element to handle this, named
compose
. It would look something like this:Note: We could enable the
src
attribute to both import and compose as a convenience. But I think importing and using are two different things and minimally need what is shown above.Open Question: Could
compose
be a processing instruction or something else besides an element? Ideally, the composition should not affect the dom structure. It's really only a location where the composition should occur.The above should all work when importing into documents or other HTML modules. One other scenario is worth mentioning: importing DCEs into a potential declarative custom element registry. That could look something like this:
And of course, you could import this entire registry into a document as follows:
This would define all the contained elements in the current document, using their specified tag names. To change the names, we could add a mapping, similar to the way export parts work in shadow dom.
Feedback Request
Ok, that's a rough, initial sketch of how we might put some pieces together. Let's start the conversation around whether this meets all the needs, what are the semantics, etc.