Playground
Put it on a page and every custom element loads itself the first time its tag shows up: on the first render, and later too, when a Datastar morph or a script adds markup. Nothing else changes, and pages ship only the components they actually use.
It is the generic version of the autoloader this site serves at /c/autoloader.js, which carries Starbase's own tag map. This one takes the map (or a URL pattern) from you, and works with any web component, not only Rocket ones.
Examples
A map of tags
<sb-autoloader modules='{"demo-badge": "/c/autoloader/demo-badge.js"}'></sb-autoloader>
<demo-badge>loaded on demand</demo-badge>
demo-badge is a plain custom element with no dependencies. Nothing on the page imports it; the autoloader fetches it because the tag is there.
A pattern for a whole folder
With a naming convention, one line covers every component. {tag} is the element's name, and match keeps it to your own tags, so unrelated elements (a third-party widget, an icon element) are left alone:
<sb-autoloader pattern="/components/{tag}/{tag}.js" match="^x-"></sb-autoloader>
<x-chart></x-chart> <!-- loads /components/x-chart/x-chart.js -->
<x-table></x-table> <!-- loads /components/x-table/x-table.js -->
Entries in modules win over the pattern, so exceptions stay easy:
<sb-autoloader
pattern="/components/{tag}/{tag}.js"
match="^x-"
modules='{"x-legacy": "https://cdn.example.com/legacy/bundle.js"}'
></sb-autoloader>
No flash of undefined elements
Custom elements are empty until their module arrives. Put a class on <html>, hide undefined elements with it, and name it in cloak: it is removed once the first round of components is defined, or after timeout (3 s by default), so a failed module can never leave the page blank.
<html class="loading">
<style>.loading :not(:defined) { visibility: hidden }</style>
<sb-autoloader pattern="/c/{tag}/{tag}.js" cloak="loading"></sb-autoloader>
Watch what it loads
sb-load fires per component. sb-ready fires once, when the components that were on the page at startup are defined; later arrivals keep firing sb-load only.
<div data-signals="{_log: 'waiting…'}">
<sb-autoloader
modules='{"demo-card": "/c/autoloader/demo-badge.js?tag=demo-card"}'
data-on:sb-load="$_log = 'loaded <' + evt.detail.tag + '>'"
data-on:sb-ready="$_log += ' · ready (' + evt.detail.loaded + ')'"
data-on:sb-load-error="$_log = 'could not load <' + evt.detail.tag + '>: ' + evt.detail.error"
></sb-autoloader>
<demo-card>a tag of its own</demo-card>
<p data-text="$_log"></p>
</div>
(The demo module takes the tag it defines from ?tag=, so this example loads something the examples above haven't.) A tag that is already defined is never fetched again, so an autoloader that finds nothing to do goes straight to sb-ready with loaded: 0.
From JavaScript, el.ready is a promise that resolves with the number of components loaded, and el.load('x-chart') fetches one by hand.
Dependencies between components
A component that renders another tag inside its own shadow root would otherwise only be discovered after it rendered. List those tags in requires and they load together:
<sb-autoloader
pattern="/c/{tag}/{tag}.js"
requires='{"sb-code-playground": ["sb-code-editor"]}'
></sb-autoloader>
Notes
- Wrapping is optional.
<sb-autoloader>works as a single tag anywhere on the page; children render untouched, so you can also wrap the markup that needs the components. - Once per tag. Two autoloaders on a page, or one that survives a morph, never fetch the same module twice. A failed load is retried when the tag appears again.
- Shadow roots. It watches the document, so components inside other components' shadow roots are discovered when those render into the page, not before.
requirescovers the rest. - Rocket components import
datastar, so the page still needs the import map for it. Plain custom elements need nothing. - A failed module reports through
reportError(sowindow.onerrorsees it) and emitssb-load-error. Amatchthat isn't a valid regular expression is reported the same way, and the pattern is skipped. demo-badge.jsin this component's folder belongs to the examples above, not to the loader; that is why it shows up in the size table. It is served at/c/autoloader/demo-badge.js, so in a pull-request preview (where the component is not in the catalog yet) the examples report a failed load: the loader works, the demo module is simply not there.
Accessibility
The element has no box of its own (display: contents) and no role; it adds nothing to the accessibility tree and takes no focus. Children, if you wrap any, render exactly where they are written. Use cloak so people don't see half-built elements, and remember the page must work if a module never arrives.
Installation
Add Datastar with Rocket and the Starbase autoloader once per page, then use the tag. The autoloader imports each component the first time its tag appears, including tags added later by a Datastar morph.
<!-- Once per page: Datastar with Rocket, and the Starbase autoloader.
It loads every <sb-…> component the first time its tag appears. -->
<script type="importmap">
{ "imports": { "datastar": "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/autoloader.js"></script>
<!-- Optional, no flash of undefined elements: class="sb-cloak" on <html>, and -->
<style>.sb-cloak :not(:defined) { visibility: hidden }</style>
<sb-autoloader modules='{"demo-badge": "/c/autoloader/demo-badge.js"}'></sb-autoloader>
<demo-badge>loaded on demand</demo-badge>
<!-- In production, pin today's catalog instead of the latest: the browser then
refuses any file that changed. Add "integrity" to the import map above: the
hashes from https://starbase.zweiundeins.gmbh/c/@722e189276ac/importmap.json and Datastar's, below. -->
<!--
<script type="importmap">
{ "imports": { "datastar": "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js" },
"integrity": { "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js": "sha384-vUxZojLrF1Ar3de5h7VINqhJXBgjyZS4U49pHvGa87kum6j5Xn6JRziuARNw5ELG", "…": "…from importmap.json" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/@722e189276ac/autoloader.js" integrity="sha384-scFa7/4jztDOx1YZT9/1g1ULYm9LsDlQn/8hMTyyD4IkPnZV2pfhHytoNTWTJG6b"></script>
-->
<!-- Or load just this component, pinned to this version. The minified module
is what the autoloader uses; the readable source is the same URL without .min. -->
<!-- <script type="module" src="https://starbase.zweiundeins.gmbh/c/autoloader@7d520cd36d58/autoloader.min.js" integrity="sha384-hwGY0BgfepzNNW5x0U3WyxujY0HawM17eb4yFuyW423pcjEE6CG9Av5IPdyFY02R"></script> -->Size
Each file compressed on its own, the way it is served (gzip -9, brotli -11). The autoloader loads the minified files (esbuild), so that last column is what a page downloads; the readable source is always there too. Datastar and Rocket are shared by every component and not counted.
| File | Original | gzip | brotli | minified |
|---|---|---|---|---|
autoloader.js | 5.6 kB | 2.3 kB | 1.9 kB | 1.3 kB |
demo-badge.js | 998 B | 643 B | 537 B | 373 B |
| Total | 6.6 kB | 2.9 kB | 2.5 kB | 1.7 kB |
API reference
Props
| Attribute | Type | Default | Description |
|---|---|---|---|
modules | json | {} | Explicit map of tag to module URL: {"x-chart": "/js/x-chart.js"}. Wins over pattern. |
pattern | string | "" | URL template for tags not in modules, with {tag}, e.g. "/components/{tag}/{tag}.js". |
match | string | "" | Regular expression the tag must match before pattern applies, e.g. "^x-". Empty: every unknown custom element. |
requires | json | {} | Tags a component renders itself, loaded with it: {"x-table": ["x-cell"]}. |
base | string | "" | Base for relative URLs. Default: the document's base URL. |
cloak | string | "" | Class removed from <html> once the first components are defined (against the flash of undefined elements). |
timeout | number | 3000 | Remove the cloak class after this many ms, whatever happens. |
Events
| Name | Description |
|---|---|
sb-load | After a component is defined. detail: { tag, url }. |
sb-load-error | When a module fails to load. detail: { tag, url, error }. |
sb-ready | Once, when the components present at startup are defined (right away when there were none). detail: { loaded }. |