add ssr-counter example demonstrating SSR + hydration end-to-end

Counter.el defines Counter, CounterPair, and App components with {#if}
conditionals, event handlers, and child component composition.

render.js (generated by elc-ui --target=server) is the Node.js render
script the server calls. app.js (generated by elc-ui --target=web) is the
compiled browser module. hydrate.js attaches the runtime to the
server-rendered DOM without replacing it.

index.html shows the integration point with instructions for injecting
server output and the two CLI commands needed to regenerate both targets.
This commit is contained in:
Will Anderson
2026-05-04 12:49:19 -05:00
parent f8e32e7719
commit 143137e2b4
6 changed files with 632 additions and 10 deletions
+49
View File
@@ -0,0 +1,49 @@
/**
* hydrate.js — Client-side hydration entry point for ssr-counter.
*
* This script runs in the browser after the server has delivered the
* SSR-rendered HTML. It:
* 1. Finds each server-rendered component root (data-el-component attribute).
* 2. Reads back the initial state snapshot (data-el-state attribute).
* 3. Attaches the el-ui runtime WITHOUT replacing the existing DOM.
* 4. Binds all event handlers so the page becomes interactive.
*
* The page is immediately visible (the SSR HTML), and becomes interactive
* as soon as this script finishes executing. There is no content flash.
*
* SSR + hydration lifecycle:
* Server: renderToString(src, 'App', props) → HTML with markers
* Network: HTML delivered to browser, painted immediately
* Client: hydrate('[data-el-component="App"]', App) → events bound
* spreading activation activates on first setState() call
*/
import { hydrate } from '../../dist/el-ui.js';
import { Counter, CounterPair, App } from './app.js';
// Hydrate each component by its data-el-component selector.
// Order matters: hydrate leaf components before their parents so
// each instance gets its own renderer and state graph.
// Hydrate all Counter instances
document.querySelectorAll('[data-el-component="Counter"]').forEach(el => {
// Read the stable ID to avoid double-hydration
if (el.dataset.elHydrated) return;
hydrate(`[data-el-id="${el.dataset.elId}"]`, Counter);
el.dataset.elHydrated = 'true';
});
// Hydrate all CounterPair instances
document.querySelectorAll('[data-el-component="CounterPair"]').forEach(el => {
if (el.dataset.elHydrated) return;
hydrate(`[data-el-id="${el.dataset.elId}"]`, CounterPair);
el.dataset.elHydrated = 'true';
});
// The App component contains CounterPair and Counter children.
// If you want the App to own the full activation graph, hydrate it here.
// For this example we hydrate individual Counter components so each
// maintains its own independent state graph (the simpler model).
// Uncomment to hydrate the whole App as one unit instead:
// hydrate('[data-el-component="App"]', App);