Skip to content

Commit e8e4383

Browse files
committed
feat(core): client hydrates data-webjs-prop-* attributes, then strips
Completes the SSR property-binding round-trip. The server emits data-webjs-prop-<kebab>="<wire-encoded>" for each .prop=${val} hole in PR-WIP commit 2c4c98e. This commit teaches WebComponent to consume those attributes on the browser side. At the top of connectedCallback (before lazy-hydration gating and before _activate), the component scans its own attributes for the data-webjs-prop- prefix. For each: * decode via the same wire serializer used on the server (rich types like Date, Map, Set, BigInt, cycles) * assign to the camelCase property (so the reactive accessor picks it up and triggers normal change-detection) * removeAttribute() so the settled DOM has no framework cruft One-time per element via a __webjsPropsHydrated guard, so disconnect/reconnect cycles do not retry decode on attributes that no longer exist. Malformed payloads warn once and are skipped; the rest of the component continues to hydrate. Net result for the user: writing .posts=${posts} in a parent template just works through SSR, including on the first paint. The DOM after hydration completes shows <post-list> with no extra attributes.
1 parent 2c4c98e commit e8e4383

1 file changed

Lines changed: 47 additions & 0 deletions

File tree

packages/core/src/component.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { render as clientRender } from './render-client.js';
22
import { isCSS, adoptStyles } from './css.js';
33
import { register, tagOf } from './registry.js';
4+
import { parse as deserializeProp } from './serialize.js';
45
import {
56
captureAuthoredChildren,
67
adoptSSRAssignments,
@@ -314,6 +315,19 @@ export class WebComponent extends Base {
314315

315316
connectedCallback() {
316317
if (!isBrowser) return;
318+
319+
// Apply any `data-webjs-prop-*` attributes emitted by SSR. The server
320+
// emits these for `.prop=${val}` bindings in parent templates so
321+
// rich-typed values (Array, Object, Date, Map, Set, BigInt, cycles)
322+
// round-trip through the rendered HTML. Once applied, the attributes
323+
// are stripped so the settled DOM matches what the user would expect
324+
// from the JS source: no framework artifacts left on the element.
325+
// One-time per element. Subsequent reconnections do nothing.
326+
if (!this.__webjsPropsHydrated) {
327+
this.__webjsPropsHydrated = true;
328+
this._hydratePropAttrs();
329+
}
330+
317331
const Ctor = /** @type any */ (this.constructor);
318332

319333
// Selective hydration: defer activation until the element scrolls into
@@ -357,6 +371,39 @@ export class WebComponent extends Base {
357371
*
358372
* @private
359373
*/
374+
/**
375+
* Read `data-webjs-prop-*` attributes (emitted by SSR for `.prop=${val}`
376+
* bindings in parent templates), decode each via the wire serializer,
377+
* assign the decoded value to the corresponding camelCase property on
378+
* this instance, and remove the attribute from the DOM. After this
379+
* runs, inspecting the element shows the same attributes the developer
380+
* would expect from the JS source.
381+
*
382+
* @private
383+
*/
384+
_hydratePropAttrs() {
385+
/** @type {string[]} */
386+
const names = [];
387+
const attrs = this.attributes;
388+
for (let i = 0; i < attrs.length; i++) {
389+
const n = attrs[i].name;
390+
if (n.startsWith('data-webjs-prop-')) names.push(n);
391+
}
392+
for (const fullName of names) {
393+
const raw = this.getAttribute(fullName);
394+
this.removeAttribute(fullName);
395+
if (raw == null) continue;
396+
const propName = camelCase(fullName.slice('data-webjs-prop-'.length));
397+
try {
398+
/** @type any */ (this)[propName] = deserializeProp(raw);
399+
} catch (err) {
400+
console.warn(
401+
`[webjs] failed to decode ${fullName} on <${this.tagName.toLowerCase()}>: ${err && err.message}`
402+
);
403+
}
404+
}
405+
}
406+
360407
_activate() {
361408
this._connected = true;
362409
const Ctor = /** @type any */ (this.constructor);

0 commit comments

Comments
 (0)