This repository has been archived on 2026-05-05. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Will Anderson 143137e2b4 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.
2026-05-04 12:49:19 -05:00

298 lines
8.3 KiB
HTML

<!DOCTYPE html>
<!--
ssr-counter/index.html — el-ui SSR + Hydration Example
In production, the content of <div id="app"> is generated by the server
calling:
const { renderToString } = require('./render.js');
const html = renderToString('App', {});
This static file simulates that by inlining the rendered HTML directly.
The client hydration script (hydrate.js) attaches to the existing DOM
without replacing it.
To see server rendering live:
node render.js App > /tmp/rendered.html
# then inline that output into the #app div below
-->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ssr-counter — el-ui SSR + Hydration</title>
<style>
:root {
--bg: #080b0f;
--bg2: #0d1117;
--bg3: #111820;
--text: #e8edf3;
--text2: #7a8a9a;
--accent: #38bdf8;
--accent-dim: rgba(56,189,248,0.12);
--accent-glow: rgba(56,189,248,0.4);
--green: #4ade80;
--red: #f87171;
--border: rgba(255,255,255,0.08);
--radius: 8px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: 'DM Mono', 'JetBrains Mono', 'Fira Code', monospace;
min-height: 100vh;
}
.app {
max-width: 900px;
margin: 0 auto;
padding: 48px 24px;
}
.app-header {
text-align: center;
margin-bottom: 48px;
}
.app-header h1 {
font-size: 32px;
font-weight: 700;
color: var(--accent);
letter-spacing: -0.03em;
margin-bottom: 8px;
}
.subtitle {
color: var(--text2);
font-size: 14px;
}
.app-main {
display: flex;
flex-direction: column;
gap: 32px;
align-items: center;
}
.counter {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 32px 40px;
min-width: 280px;
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.counter-label {
font-size: 14px;
color: var(--text2);
text-transform: uppercase;
letter-spacing: 0.1em;
font-weight: 600;
}
.count-display {
display: flex;
align-items: center;
justify-content: center;
}
.count {
font-size: 72px;
font-weight: 700;
font-variant-numeric: tabular-nums;
min-width: 120px;
text-align: center;
letter-spacing: -0.03em;
transition: color 0.15s;
}
.count.zero { color: var(--text2); }
.count.positive { color: var(--accent); text-shadow: 0 0 40px var(--accent-glow); }
.count.negative { color: var(--red); text-shadow: 0 0 40px rgba(248,113,113,0.4); }
.buttons {
display: flex;
gap: 12px;
}
.btn {
background: var(--accent-dim);
border: 1px solid var(--accent);
color: var(--accent);
font-size: 18px;
font-family: inherit;
padding: 0 20px;
height: 48px;
border-radius: var(--radius);
cursor: pointer;
transition: background 0.15s, box-shadow 0.15s;
display: flex;
align-items: center;
justify-content: center;
}
.btn:hover {
background: rgba(56,189,248,0.22);
box-shadow: 0 0 16px var(--accent-glow);
}
.btn:active { transform: scale(0.95); }
.btn-reset {
font-size: 12px;
letter-spacing: 0.06em;
text-transform: uppercase;
background: rgba(255,255,255,0.04);
border-color: var(--border);
color: var(--text2);
}
.btn-reset:hover {
background: rgba(255,255,255,0.08);
box-shadow: none;
}
.btn-decrement {
color: var(--red);
border-color: var(--red);
background: rgba(248,113,113,0.08);
}
.btn-decrement:hover {
background: rgba(248,113,113,0.18);
box-shadow: 0 0 16px rgba(248,113,113,0.3);
}
.history {
font-size: 12px;
color: var(--text2);
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
justify-content: center;
}
.history-label {
color: var(--text2);
opacity: 0.6;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.1em;
}
code {
color: var(--accent);
font-family: inherit;
}
.hint {
font-size: 12px;
color: var(--text2);
opacity: 0.5;
}
.counter-pair {
display: flex;
gap: 24px;
flex-wrap: wrap;
justify-content: center;
}
.el-badge {
position: fixed;
bottom: 16px;
right: 16px;
font-size: 10px;
color: var(--text2);
opacity: 0.5;
letter-spacing: 0.1em;
text-transform: uppercase;
}
/* SSR indicator — shows before hydration completes */
.ssr-note {
text-align: center;
font-size: 11px;
color: var(--text2);
opacity: 0.4;
margin-bottom: 24px;
letter-spacing: 0.06em;
}
</style>
</head>
<body>
<!--
In a real server setup, this div's content is injected by the server:
const { renderToString } = require('./render.js');
res.send(`<div id="app">${renderToString('App', {})}</div>`);
To preview server output:
node render.js App
Below is the static simulation of that output.
The data-el-* attributes are the hydration markers written by the SSR engine.
-->
<div id="app">
<!-- SSR output placeholder. Run: node render.js App and paste here. -->
<!-- Or open this file with a server that injects the SSR output above. -->
<!--
Example of what the server renders (simplified for readability):
<div class="app"
data-el-component="App"
data-el-id="el-app-1"
data-el-state="{}">
<header class="app-header">
<h1>el-ui SSR + Hydration</h1>
<p class="subtitle">Server-rendered, client-hydrated with spreading activation</p>
</header>
<main class="app-main">
<div class="counter"
data-el-component="Counter"
data-el-id="el-counter-1"
data-el-state='{"count":0,"history":""}'>
...
</div>
</main>
</div>
-->
</div>
<p class="ssr-note">el-ui SSR + hydration · no DOM replacement on hydrate</p>
<div class="el-badge">el-ui · spreading activation</div>
<!--
Hydration script.
- app.js: the compiled component classes (web target, for render() and setState())
- hydrate.js: attaches the runtime to the server-rendered DOM
Both are type="module" so they run after the document is parsed.
The SSR HTML is already painted before these scripts execute.
-->
<script type="module" src="./hydrate.js"></script>
<!--
Integration note for the El web server:
===========================================
The El web server (C binary) renders pages by calling:
node render.js App '{}'
and capturing stdout as the HTML to inject into the template above.
This is the simplest bridge: no Node.js server needed, just exec().
A tighter integration embeds Node.js (via libnode or napi) and calls
renderToString() directly without subprocess overhead — appropriate for
high-traffic pages. The render script API is identical either way.
-->
</body>
</html>