Archived
143137e2b4
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.
298 lines
8.3 KiB
HTML
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>
|