renderable-element
Composition base for Neutron elements that defer rendering a <template>
until the right moment. It owns the load + render lifecycle so subclasses
only decide when to flip is-active. Used by <include-content>,
<spa-route>, and any custom element you compose yourself.
The demos below use <include-content> (the simplest concrete subclass)
to exercise behavior that comes straight from this mixin.
Features
- Template resolution via
template-refin-document selectors or remote URLs - Prefetch strategies
lazy(default),eager, oridle - Configurable render host light DOM, shadow, author iframe, or any selector
- Cancelable render / unrender parents can wrap updates in view transitions
- Persistable content keep live subtree state across unrender / render cycles
- Ready coordination
ready-on+delaying-readyfor paint-synced reveals
Installation
This package is available in the
<script src="https://unpkg.com/@excom/kit-utils@0.3.0/dist/index.umd.min.js"></script>
<script src="https://unpkg.com/@excom/neutron@0.2.0/dist/index.umd.min.js"></script>
<script src="https://unpkg.com/@excom/renderable-element@0.3.0/dist/index.umd.min.js"></script>npm install @excom/renderable-elementimport { /* … */ } from "@excom/renderable-element";Peer dependencies (0)
Packages a consumer must install alongside this one. Workspace deps are bundled.
| Package | Version |
|---|---|
|
Usage
Compose RenderableElement into a Neutron class and toggle isActive
from whatever signal makes sense — a media query, a websocket message,
an experiment flag, etc. Everything else (template fetch, caching, host
resolution, lifecycle events) is inherited.
import { Neutron } from "@excom/neutron";
import { RenderableElement } from "@excom/renderable-element";
export const MediaGated = Neutron.compose([
RenderableElement,
Neutron({
tag: "media-gated",
props: {
mediaQuery: String,
},
}),
])
.onPropChanged("mediaQuery", (el, prev) => {
prev.mediaQuery && el._mql?.removeEventListener("change", el._sync);
if (!el.mediaQuery) return { isActive: false };
el._mql = window.matchMedia(el.mediaQuery);
el._sync = () => (el.isActive = el._mql.matches);
el._mql.addEventListener("change", el._sync);
el._sync();
});
MediaGated.define();import { Neutron } from "@excom/neutron";
import { RenderableElement } from "@excom/renderable-element";
export const MediaGated = Neutron.compose([
RenderableElement,
Neutron({
tag: "media-gated",
props: {
mediaQuery: String,
},
}),
])
.onPropChanged("mediaQuery", (el, prev) => {
prev.mediaQuery && el._mql?.removeEventListener("change", el._sync);
if (!el.mediaQuery) return { isActive: false };
el._mql = window.matchMedia(el.mediaQuery);
el._sync = () => (el.isActive = el._mql.matches);
el._mql.addEventListener("change", el._sync);
el._sync();
});
MediaGated.define();<media-gated media-query="(min-width: 900px)">
<template>
<wide-screen-only></wide-screen-only>
</template>
</media-gated><media-gated media-query="(min-width: 900px)">
<template>
<wide-screen-only></wide-screen-only>
</template>
</media-gated>Event names are prefixed with the concrete tag (shown as {tag}-… in the
API). For <include-content> that means include-content-render, etc.
API Reference
Attributes
Role —option is configurable, state
is managed by the element (read-only), or hybrid which is both.
Provision
Thisprovision property lives on the DOM node.
Read / watch it with Quark’s prop("provision"), or listen
for the neutron-provision event from app JS.
Events
Type
command event (HTML Command API): from a
<button command commandfor>, an <event-handler
command-name>, or a CommandEvent.
Recognized Elements
Child or descendant elements are recognized by and relevant to its functionality.Styles
@custom-selector synonyms so custom elements can share
semantic styles. Element aliases match the host tag / .tag-* class; state
aliases match attributes (nested under the element alias).
Attributes (11)
Role —option is configurable, state
is managed by the element (read-only), or hybrid which is both.
bypass-cache
template-ref only).
bypassCache
boolean
falsehost-ref
shadow attaches an open shadow root. iframe paints into a child <iframe data-render-host> body (you supply the iframe — useful for sandboxed / third-party document isolation). Any other value is a portal selector.
hostRef
string
"shadow" | "iframe" | <CSS Selector>nullpersist-content
_persistedTree) so form values, scroll position, and subtree state survive toggles.
persistContent
boolean
falsepre-fetch
"" aliases eager. idle never runs in a prerender.
preFetch
string
"" | "eager" | "idle" | "lazy""lazy"ready-on
delaying-ready is set so CSS can hide the host for a coordinated paint / view transition. Prerendered content kept at hydration is ready at once, never hidden.
readyOn
string
<Event Name>nulltemplate-ref
<template> — in-document selector or remote URL. Changing mid-flight aborts and reloads. Can use :scope to relatively select elements: e.g. main:has(:scope) > template
templateRef
string
<CSS Selector> | <URL>":scope > template"is-active
isActive
boolean
falsedelaying-ready
render and the matching ready-on event (or a failed load). Hook with CSS for coordinated paints / view transitions.
delayingReady
boolean
falsedid-load
is-active toggles so consumers know later paints are warm (URL refs reuse the shared fetch cache in kit-utils). Cleared when template-ref changes or --reload forces a fresh resolve.
didLoad
boolean
falseis-error
error event.
isError
boolean
falseis-loading
isLoading
boolean
falseProvision (0)
Thisprovision property lives on the DOM node.
Read / watch it with Quark’s prop("provision"), or listen
for the neutron-provision event from app JS.
Events (7)
e.preventDefault() is not
synchronously called on the event.
Type
{tag}-aborted
is-active was unset (via startTeardown).
Type
RenderableAbortedEvent
{tag}-did-render
Type
RenderableDidRenderEvent
{tag}-did-unrender
Type
RenderableDidUnrenderEvent
{tag}-error
AbortError.
Type
RenderableErrorEvent
{tag}-render
event.detail is a thunk that performs the load (if not already loaded) and renders the children, returning a Promise that resolves once the corresponding ready-on event fires (or immediately if ready-on is unset). The promise rejects if the template fails to load, host-ref resolves to no host (nor an author iframe still loading), or the element is torn down mid-flight (startTeardown while loading / delaying-ready). Call preventDefault() to defer rendering and invoke event.detail() later.
Type
RenderableRenderEvent
event.detail() to load (if needed) and render the template into the host.{tag}-unrender
event.detail is a thunk that removes the rendered children. Call preventDefault() to defer the removal. Not fired when teardown cancels an in-flight load — that path emits aborted instead.
Type
RenderableUnrenderEvent
event.detail() to remove rendered children from the host.
command event (HTML Command API): from a
<button command commandfor>, an <event-handler
command-name>, or a CommandEvent.
--reloadRecognized Elements (2)
Child or descendant elements are recognized by and relevant to its functionality.host-ref="iframe". Content paints into iframe.contentDocument.body. Provide your own iframe (e.g. with srcdoc); the element will not create one.
<template> child used when template-ref is the default ":scope > template". Not required when template-ref points at a selector or URL elsewhere.
Styles (0)
all: revert-layer.
Valence.css ships this as .unstyled and .unstyled-all for the entire tree.
@custom-selector synonyms so custom elements can share
semantic styles. Element aliases match the host tag / .tag-* class; state
aliases match attributes (nested under the element alias).
Release notes (3)
0.3.0
- Add hydration of prerendered content: an element whose host already holds the server's render of the same template does not render again, fires
did-renderonce and is ready at once (neverdelaying-ready);persist-contentkeeps those nodes across toggles andpre-fetch="idle"does not run during a prerender - Fix an issue where a first mount into a
host-refselector cleared the host and fireddid-unrender: what the host holds now stays until the first render replaces it
0.2.0
- Update a URL
template-refthat answers with an error status to setis-errorand fire the error event, paint nothing and log one error with the status as its cause; the error page is no longer rendered as content - Fix an issue where a failed template load held
readyuntilrender-timeout: the promise fromrenderrejects at once anddelaying-readyclears - Fix an issue where the promise from
rendernever settled anddelaying-readystayed set whenhost-refresolved to no host: the promise now rejects at once anddelaying-readyclears, while an author iframe still loading keeps waiting
0.1.1
- Ship only dist (and declared extras) in the npm tarball; drop build logs, tests and sources
Release notes
e.preventDefault() is not
synchronously called on the event.
all: revert-layer.
Valence.css ships this as .unstyled and .unstyled-all for the entire tree.
Examples
Choosing a render host
Unset host-ref renders into the element's light DOM. "shadow" attaches
an open shadow root for style isolation:
"iframe" paints into a child <iframe data-render-host> you provide —
sandbox styles / scripts / document context. Only nodes move: custom
elements upgrade there only if the iframe document loads their definitions:
Any other value is a CSS selector — the template lands in whatever element it resolves to:
On a shadow and iframe hosts are not prerendered and render in the browser.
Hooking render with view transitions
render and unrender are cancelable; event.detail is the update
thunk. A parent can preventDefault() and run the mutation inside
document.startViewTransition() — the same pattern <spa-manager>
uses to batch sibling routes.
The thunk's returned Promise resolves when the view is ready (immediately, or when ready-on fires). If is-active is unset while still loading / delaying-ready, teardown rejects that Promise and emits aborted instead of unrender.
Pair with ready-on so delaying-ready stays set until your transition
has committed:
<media-gated ready-on="my-app-paint" media-query="(min-width: 900px)">
<template>...</template>
</media-gated><media-gated ready-on="my-app-paint" media-query="(min-width: 900px)">
<template>...</template>
</media-gated>media-gated[delaying-ready] {
display: none;
}media-gated[delaying-ready] {
display: none;
}Persisting content across cycles
Once a template resolves, did-load stays set so consumers know later
toggles are warm — URL template-refs reuse the shared fetch cache in
kit-utils. Without persist-content (the default), each activation
re-resolves and imports a fresh clone — subtree state is lost on
unrender. With it, the same live nodes are held across toggles:
On a prerendered page the nodes it holds are the prerendered ones.
Loading strategies
pre-fetch controls when the template is fetched, separately from
when it is rendered:
<!-- default: fetch on first activation -->
<my-el></my-el>
<!-- pre-warm immediately on attribute set -->
<my-el pre-fetch="eager"></my-el>
<!-- backfill on idle -->
<my-el pre-fetch="idle" template-ref="/fragments/hero.html"></my-el><!-- default: fetch on first activation -->
<my-el></my-el>
<!-- pre-warm immediately on attribute set -->
<my-el pre-fetch="eager"></my-el>
<!-- backfill on idle -->
<my-el pre-fetch="idle" template-ref="/fragments/hero.html"></my-el>Pair with bypass-cache for revalidation when the element activates
multiple times. Invoke the --reload command to force a refresh.
pre-fetch="idle" never runs in a prerender: the template is fetched in the browser.