Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exploration: HTML Module Imports and Exports #1059

Open
EisenbergEffect opened this issue May 3, 2024 · 13 comments
Open

Exploration: HTML Module Imports and Exports #1059

EisenbergEffect opened this issue May 3, 2024 · 13 comments

Comments

@EisenbergEffect
Copy link
Contributor

EisenbergEffect commented May 3, 2024

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:

  • Templates
  • Fragments
  • Styles
  • Custom Elements
  • Element Registries

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 the export attribute. Elements with only an id can be referenced within the module, but are not able to be imported. Both id and export are required for that.

Here are a few examples.

<!-- Becomes an export named pricingCard of type HTMLTemplateElement -->
<template export id="pricingCard">
  <div class="pricing-card">
    <div class="header">
      <h4 class="name"></h4>
    </div>
    <div class="body">
      <h1 class="title"></h1>
      <ul class="features">
      </ul>
      <button type="button" class="subscribe-action"></button>
    </div>
  </div>
</template>

<!-- Becomes an export named sharedHeaderStyles of type CSSStyleSheet -->
<style export id="sharedHeaderStyles">
  .shared-header {
    display: flex;
    justify-content: center;
  }
</style>

<!-- Becomes an export named sharedHeader of type DocumentFragment -->
<header class="shared-header" export id="sharedHeader">
  <ul>
    <li><a href="./home">Home</a></li>
    <li><a href="./features">Features</a></li>
    <li><a href="./pricing">Pricing</a></li>
    <li><a href="./faq">FAQs</a></li>
    <li><a href="./about">About</a></li>
  </ul>
</header>

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.

import { 
  pricingCard, 
  sharedHeaderStyles,
  sharedHeader
} from "./my-resources.html" with { type: "html" };

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 an export attribute. We could also enable an element with an export and no id to be imported as the default export.

Importing HTML Modules in HTML

Importing into HTML involves several tasks:

  • Importing the resources
  • Declaratively using a DCE, template, or fragment
  • Defining a DCE

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:

<import from="./my-resources.html#sharedHeader">
<import from="./my-resources.html#pricingCard" as="productCard">
<import from="./design-system.html#MyButton" as="ui-button">

The from attribute provides the path to the HTML module and also includes a fragment identifying the specific export. The as 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 an id denoted by as. In the case of a custom element, it will call define with the value of as for the tag name.

To use an imported custom element, is no different than using any custom element:

<ui-button>Click Me!</ui-button>

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.

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:

<registry export id="my-registry">
  <import from="./my-element.html" as="your-element">
  <import from="./my-module.html#NamedElementTwo" as="element-two">
  <import from="./everything-from-another-registry.html">
  
  <element tag="an-inline-element"></element>
</registry>

And of course, you could import this entire registry into a document as follows:

<import from="./my-registry.html">

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.

@EisenbergEffect EisenbergEffect changed the title Idea: HTML Module Imports and Exports Exploration: HTML Module Imports and Exports May 3, 2024
@justinfagnani
Copy link
Contributor

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.

Importing identifiers

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 need to define exactly what an "identifiers" is. Is it an id and works with idrefs? Is it a separate namespace from ids? Is there some kind of "lexical" scoping mechanism for identifiers to facilitate bundling?

Declaring local identifiers

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

Using identifiers

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>

@jaredcwhite
Copy link

@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?

@EisenbergEffect
Copy link
Contributor Author

@jaredcwhite Yes, I was thinking we could still use that in script tags within the module.

@o-t-w
Copy link
Contributor

o-t-w commented May 8, 2024

@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.

@EisenbergEffect
Copy link
Contributor Author

@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.

@sashafirsov
Copy link

sashafirsov commented May 10, 2024

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.

@EisenbergEffect
Copy link
Contributor Author

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>

@justinfagnani
Copy link
Contributor

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.

@Jamesernator
Copy link

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>

@NullVoxPopuli
Copy link

Why this over document[id] or document.querySelector('#theID')?

@o-t-w
Copy link
Contributor

o-t-w commented May 12, 2024

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. #645 (comment)

@Jamesernator
Copy link

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.

@DanielHerr
Copy link

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>?

https://sergey.trysmudford.com/slots/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants