javascript-web-component-hydration · npm web-component-hydration 1.0.5
Placeholder in. Live element out.
Two ES modules. CustomHTMLElement is an HTMLElement
subclass that clones a template into an open shadow root;
hydrate() is the function that swaps server-rendered placeholders for
instances of it.
the surface
three exportsCustomHTMLElement- Extends
HTMLElement. Subclasses declare a tag name with a staticisgetter and are defined once per page with the staticregister(), which wrapscustomElements.whenDefined()and rejects when no name is set orwindow.customElementsis missing. hydrate(name, slotElementType?)- Finds every
[data-template="name"]in the document and replaces it with a freshly created<name>element, carrying over the initial data. The optional second argument is the tag used for slot content created from that data —spanby default. onReady(callback)- A wrapper around
DOMContentLoadedthat calls back immediately if the document has already finished loading.
The constructor does three things in order: it looks up
document.getElementById('template-' + is), calls
this.attachShadow({ mode: 'open' }), and initialises a list of child
element names it has already hydrated. On connectedCallback it appends a
clone of that template's content to the shadow root, then hydrates any
[data-template] descendants — in the light DOM and in the shadow root both
— so nested components resolve too.
the markup contract
two attributes, one idThe library does no rendering on the server itself; it is written to pair with a
service that does. It needs two things in the page: a client template whose
id is template- plus the element name, and a server-rendered
placeholder whose data-template attribute is that same name. The README's
example template and its rendered form, quoted:
<template id="template-x-list-item">
<li>
<slot></slot>
</li>
</template>
<div data-template="x-list-item" data-initial-data="content">
<li>
<slot>content</slot>
</li>
</div>
The data (content) becomes the data-initial-data value and is
inserted into the unnamed slot as a child. Named slots require the value to be
URL-encoded JSON keyed by slot name; the README's named-slot variant:
<div data-template="x-list-item" data-initial-data="%7B%22label%22%3A%22content%22%2C%22foo%22%3A%22bar%22%7D">
<li>
<slot name="label">content</slot>
<slot name="foo">bar</slot>
</li>
</div>
How the data is decoded. hydrate() runs
decodeURI() and then the substitutions PHP's urlencode()
needs — %3D to =, + to space,
%2F to /, %3A to :,
%2C to , — and tries
JSON.parse(). On success each key becomes an element with
slot = key and the value as innerHTML. On failure the whole
decoded string is treated as one unnamed slot's HTML. Note that this writes slot
content with innerHTML: the initial data is treated as markup, not as
inert text.
install
npm, no build stepThe package is web-component-hydration on npm; 1.0.5 is the latest
version and was published on 2019-08-06. It has no dependencies,
module points at index.js, and the source is three ES
modules — custom-html-element.js, helpers.js, and an
index.js that re-exports both.
The repository's README documents usage, not installation, so no install command is
printed here. Its own examples import by relative path from
./node_modules/web-component-hydration/…, which is what the sibling PHP
demo does: its examples/simple/js/package.json depends on
web-component-hydration@^1.0.0 and its README runs
cd examples/simple && yarn install. The lockfile there resolves the
dependency to exactly 1.0.5.
one example, quoted
register, then hydrateThe whole client side of a component, from the README. First the element:
// x-list-item.js
import { CustomHTMLElement } from './node_modules/web-component-hydration/custom-html-element.js';
export class XListItem extends CustomHTMLElement {
// define `XListItem`'s tag name
static get is() { return 'x-list-item'; }
}
Then the entry point — register the element, then hydrate the placeholders that carry its name:
// main.js
import { hydrate } from './node_modules/web-component-hydration/helpers.js';
import { XListItem } from './x-list-item.js';
// register `XListItem` as a custom element
await XListItem.register();
// hydrate any server-side rendered `XListIem`s
hydrate(XListItem.is);
Order matters: hydrate() returns early if the element is not defined yet
or if template-<name> is absent, so a hydrate call that runs before
register() resolves does nothing at all. The PHP demo wraps the whole
sequence in onReady() and awaits each register() before
calling hydrate() for that component.
Both snippets are quoted as written, including the README's XListIem
typo in the trailing comment. The snippets were not run on their own, but the PHP demo
they come from was. It was served with php -S on PHP 8.5.11 from
php-web-component-ssr at 00be84d, with version 1.0.5 of the
package from npm (its checksum matches the demo's lockfile), and opened in Chromium,
Firefox and WebKit. In all three, no data-template placeholder was left.
Each element had a shadow root holding its template, and the page logged no
errors.
One thing the README doesn't mention is that initial data is added even when the
template has no slot for it. The demo's foo: bar became
<span slot="foo">bar</span>. It is in the element's markup
but never shown.
status and limits
2019 codeVersion 1.0.5, MIT licensed. The last commit that changes behaviour is 2019-08-06 (the v1.0.5 tag); the repository's newest commit, 2026-05-08, only adds a homepage link. It is published and unmaintained, not deprecated.
Two consequences follow from how it works, not from a defect.
Hydration needs JavaScript. The browser receives a div with copied
children; the shadow root and the real custom element only exist after the module runs.
That is the same trade the PHP page describes, seen from the other side.
It is not declarative shadow DOM. Neither this repository's code nor its README mentions
<template shadowrootmode>; the shadow root here is created
imperatively with attachShadow() in the constructor, in the browser.
where to go
the other halves-
SSR in PHP
The server side of this contract, and the markup it produced when it was run.
-
Authoring in JSX (WIP)
A different base class, with the same constructor-and-shadow-root pattern.
-
Source on GitHub
Three files, 178 lines including comments, with
@ts-checkannotations. -
npm
web-component-hydration, latest 1.0.5.