Bumps spec version to 1.2.0. Documents: - SSR engine in runtime/src/ssr.js (Node.js, fully implemented) - Hydration markers (data-el-component, data-el-id, data-el-state) - hydrate() vs mount() and when to use each - Full SSR + hydration lifecycle with code examples - elc-ui CLI --target=server and --target=web flags - Section 11: complete SSR + hydration walkthrough - Section 12: versioning roadmap updated to reflect v0.1 actual state - Section 9: clarifies that SSR is JS-implemented, Rust PlatformBackend is planned
36 KiB
el-ui Framework Specification
Version 1.2.0 — May 4, 2026
Overview
el-ui is a frontend framework where state is an Engram graph and reactivity is spreading activation. Every framework must answer the same question: what re-renders when state changes? el-ui's answer is: whatever the spreading activation algorithm determines is relevant.
| Framework | Reactivity model |
|---|---|
| React | Virtual DOM diffing |
| Vue | Dependency tracking (Proxy-based reactive primitives) |
| Svelte | Compile-time static analysis |
| Solid | Fine-grained signal-based subscriptions |
| el-ui | Spreading activation over a typed state graph |
The core insight: component state is represented as a graph of related nodes, and the same spreading activation algorithm used by the Engram knowledge engine determines which components need to update. Reactivity is not declared, tracked, or diffed — it is activated and propagated, exactly as associative memory works in biological neural networks.
1. Activation-Based Reactivity Model
1.1 The Model
Every piece of state in an el-ui application is a node in an in-browser Engram graph. Nodes have:
| Field | Type | Description |
|---|---|---|
id |
String (UUID) | Node's permanent identity |
type |
String | 'state', 'route', or user-defined |
name |
String | State variable name |
content |
Any | Current value |
importance |
Float [0, 1] | Salience weight; boosted on update |
Nodes are connected by directed edges with:
| Field | Type | Description |
|---|---|---|
weight |
Float [0, 1] | Connection strength |
relation |
String | Semantic label of the relationship |
1.2 Activation Formula
When state changes via setState(), spreading activation runs from the changed node through the graph:
strength = parent_strength × edge.weight × target.importance
This formula is identical to the Engram core spreading activation formula, with the semantic similarity factor (cosine_sim) omitted because the in-browser graph does not yet carry embedding vectors. In a future version connected to the Engram server, the full four-factor formula applies:
strength = parent_strength × edge.weight × target.importance × cosine_sim(query, target)
1.3 Why Multiplication, Not Addition
Addition allows many weak signals to accumulate into false relevance. The brain's associative memory is conjunctive: an activated path requires ALL its links to be strong. Multiplication enforces this. If any factor is near zero — weak edge, dormant node, or irrelevant connection — the path dies immediately. This is not a performance optimization; it is a semantic property of associative retrieval.
1.4 Pruning Threshold
Paths with activation strength below PRUNE_THRESHOLD (default: 0.01) are cut. This prevents exponential blowup in large state graphs and models the brain's attention filter. All nodes not reached above the threshold are not considered for re-render.
1.5 Importance Boost (Long-Term Potentiation Analog)
When a state node is updated, its importance increases by 0.1 (capped at 1.0):
node.importance = Math.min(1.0, node.importance + 0.1)
Recently-changed state becomes more salient — it activates more easily in future propagation. This mirrors long-term potentiation: frequently-used pathways become lower-resistance. State that changes often gains importance; state that never changes fades toward background salience.
1.6 Activation Surface
The activation surface is the set of nodes reached during a spreading activation pass, i.e., all nodes with strength > PRUNE_THRESHOLD. Components subscribed to any node in the activation surface receive update notifications and re-render.
2. Component Definition Syntax
Components are defined in .el files (shared extension with the El programming language). A component is a specialized el module with four optional sections.
2.1 Component Structure
component ComponentName {
props {
// prop declarations
}
state {
// state declarations
}
fn methodName(param: Type) -> ReturnType {
// method body
}
template {
// HTML template
}
}
All four sections are optional. Components with no template render an empty string. Multiple fn blocks are allowed; multiple props or state blocks are not (only the last one is used).
2.2 Props
Props are read-only inputs from the parent component.
props {
label: String // required prop
variant: String = "primary" // optional with default
disabled: Bool = false // boolean with default
onClick: Fn() -> Void // function prop
}
Supported prop types: String, Int, Float, Bool, Fn(...) -> T.
Note: Array ([T]) and optional (T?) type syntax is not currently parsed by the compiler. The parser reads a single identifier as the type name.
Props are accessed in generated code as this._props_{name} (not this.props.name). In method and template bodies, the compiler exposes each prop as a local constant named {name}.
2.3 State
State declarations create nodes in the component's Engram graph. Every state variable requires an initial value.
state {
count: Int = 0
label: String = "hello"
active: Bool = false
}
Each state variable becomes:
- A node in
this._graph(seeded at construction time withtype: 'state'), tracked by ID inthis._stateNodes. - A mirrored value in
this._statefor direct read access. - A reactive binding: changing the value via
setState()triggers activation and re-render.
In method and template bodies, the compiler exposes each state variable as a local constant named {name} (reading from this._state).
2.4 Methods
Methods are fn definitions inside the component body. State assignments in method bodies compile to setState() calls.
fn increment() -> Void {
count = count + 1 // compiles to: __self.setState('count', count + 1)
}
The generated JavaScript method exposes all state variables as local const bindings before executing the body. The variable __self refers to the component instance.
3. Template Syntax
3.1 Interpolation
Any expression embedded in {expr}:
<h1>{count}</h1>
<p>{"Hello, " + name + "!"}</p>
<span>{active ? "on" : "off"}</span>
3.2 Event Binding
DOM events bound with on:event={handler}:
<button on:click={() => count = count + 1}>+</button>
<input on:input={(e) => value = e.target.value} />
<div on:mouseenter={() => hovered = true} on:mouseleave={() => hovered = false} />
Supported events: click, input, change, mouseenter, mouseleave, keydown, keyup, keypress, focus, blur, submit, mousedown, mouseup, dblclick, contextmenu.
The compiler emits data-el-{event} attributes on elements. The renderer binds handlers at mount time using new Function(...) evaluated in the component's scope (__self), and rebinds after each patch.
3.3 Dynamic Attributes
<div class={active ? "btn-active" : "btn"}></div>
<a href={currentUrl}></a>
3.4 Static Attributes
<div class="container"></div>
<input type="text" />
3.5 Boolean Attributes
The parser recognises a fixed set of boolean attribute names: disabled, checked, readonly, required, multiple, selected. These emit the attribute name when the expression is truthy, nothing when falsy.
<button disabled={isDisabled}>Submit</button>
<input checked={isChecked} />
A standalone attribute with no value (e.g., <button disabled>) is also parsed and treated as BoolAttr { expr: "true" }.
3.6 Component Invocation
Uppercase-initial tags are el-ui components; lowercase tags are HTML elements:
<Button label="Click me" variant="primary" onClick={() => handleClick()} />
<Counter initialValue={5} />
<UserCard name={currentUser.name} />
The compiler emits ${__self._child(ComponentClass, { prop: value })} for component references. Child components share the parent's activation graph via _child().
3.7 Conditional Rendering — {#if}
{#if loggedIn}
<Dashboard />
{:else}
<Login />
{/if}
Compiles to a ternary template literal expression: ${(condition) ? ...:...}.
3.8 List Rendering — {#each}
{#each users as user}
<UserCard name={user.name} />
{/each}
The items expression must evaluate to an array. Compiles to ${(items).map((user) => ...).join('')}.
3.9 Semantic Activation — {#activate} (Novel Construct)
{#activate "recent premium subscribers" as results}
<UserCard name={results.name} />
{/activate}
{#activate} runs a semantic query against the component's state graph at render time. The query string is a natural language description. The runtime:
- Calls
this._graph.search("query")which performs a string-based substring match on nodenameandcontentfields. - Sorts results by score (combination of name/content match and node importance).
- Renders the template body once for each result, binding the result as the variable name.
Compiles to:
${((this._graph.search("query")) || []).map((results) => `...template...`).join('')}
In v0.1, the search is string-based (substring match on name and content.toString()). In future versions connected to an Engram database, {#activate} will use embedding vectors and cosine similarity.
Note on business logic protection: The spec previously described AES-256-GCM encryption of {#activate} query strings in sealed production bundles. This feature is not implemented — there is no --target prod flag in the current CLI and no el seal command.
4. State Graph
4.1 Graph Structure
Each component instance owns an Engram graph (this._graph). State nodes are seeded at construction. The graph is shared between a component and its children via _child() — they operate on the same activation surface.
4.2 State Node Creation
const nodeId = graph.seed({ type: 'state', name: 'count', content: 0 });
// importance defaults to 0.5 if omitted
Returns a UUID string that permanently identifies the node.
4.3 Edge Creation
graph.connect(fromId, toId, { weight: 0.8, relation: 'derived' });
Connects two nodes with a directed edge. Higher weight = stronger activation path. Default weight is 1.0, default relation is 'related'.
4.4 Node Retrieval
const node = graph.get(nodeId);
// Returns: { id, type, name, content, importance, edges: [edgeId, ...] }
Note: the runtime method is graph.get(id), not graph.getNode(id).
4.5 State Update
this.setState('count', newValue);
// or directly:
this._graph.update(nodeId, newValue);
setState looks up the node by state variable name, calls this._graph.update() which:
- Updates
content. - Increments
importanceby 0.1 (capped at 1.0). - Runs spreading activation from the changed node.
- Notifies all subscribers in the activation surface.
- Notifies direct subscribers on the changed node.
4.6 Activation Subscription
const unsubscribe = graph.subscribe(nodeId, (node) => {
// called whenever this node enters the activation surface
});
unsubscribe(); // stop listening
Components subscribe to their state nodes in the constructor. When spreading activation reaches a subscribed node, the subscriber callback fires and the component re-renders via this._renderer.patch().
4.7 Semantic Search
const results = graph.search("recent items", "state");
// Returns: [{ id, type, name, content, importance, score }] sorted by score
// nodeType argument is optional — filters by node.type if provided
In v0.1, search is string-based: name.includes(query) or content.toString().includes(query). Score is computed as (nameMatch ? 0.6 : 0) + (contentMatch ? 0.4 : 0) multiplied by node.importance.
4.8 Graph Dump (Debug)
const allNodes = graph.dump();
// Returns: array of all node objects — useful for DevTools / debugging
5. Spreading Activation Implementation (In-Browser)
The in-browser spreading activation algorithm in graph.js (and standalone utilities in activation.js) mirrors engram-core/src/activation.rs:
// Within graph.activate(seedId, maxDepth = 3, pruneThreshold = 0.01)
// Returns: Set<string> of activated node IDs
while (queue.length > 0) {
// Best-first: process highest-strength candidate
const { id, strength, depth } = bestInQueue();
if (depth >= maxDepth) continue;
for (const edgeId of node.edges) {
const edge = this.edges.get(edgeId);
const target = this.nodes.get(edge.to);
// Multiplicative formula
const targetStrength = strength * edge.weight * Math.max(0, target.importance);
if (targetStrength <= pruneThreshold) continue;
// Winner-take-most
if (targetStrength > prevBest) { /* enqueue */ }
}
}
Key parameters:
maxDepthdefault: 3 (not 4)pruneThresholddefault: 0.01limitdefault: 20 (applies tospreadActivation()utility;graph.activate()returns a Set with no limit)
Correspondence with engram-core:
- Same best-first BFS traversal
- Same multiplicative strength formula
- Same pruning threshold semantics (0.01)
- Same winner-take-most rule (strongest path to each node wins)
- Same depth limit semantics
The in-browser graph omits the cosine_sim factor because embedding vectors are not yet available in the browser.
5.1 Standalone Activation Utilities (activation.js)
import { spreadActivation, activationStrength, reachableNodes, PRUNE_THRESHOLD } from './el-ui.js';
// Multi-seed activation — returns sorted array of reached nodes (excluding seeds)
const reached = spreadActivation(graph, [seedId1, seedId2], { maxDepth: 3, limit: 20 });
// Returns: [{ nodeId, strength, hops, node }]
// Point-to-point strength — returns 0 if no path within maxDepth
const strength = activationStrength(graph, fromId, toId, maxDepth);
// All reachable nodes from a seed (includes seed) — returns Set<string>
const reachable = reachableNodes(graph, seedId, maxDepth);
6. Router — Graph-Based Routing
6.1 Overview
Routes are nodes in the application graph of type 'route'. Navigation activates the target route node. Components subscribed to routing update automatically via spreading activation.
Route nodes start with importance: 0.3. Navigating to a route boosts its importance by 0.2 (capped at 1.0), modeling browser history salience.
6.2 Route Node Connections
The router connects parent routes to child routes via edges (weight: 0.8, relation: 'subroute'). Navigating to /about/team activates the /about route node as well — activation spreads upward through the subroute chain.
6.3 Path Matching
- Exact match:
/aboutmatches/about - Prefix match (longest wins):
/aboutmatches/about/team - Wildcard:
'*'catches all unmatched paths
6.4 API
const router = new Router(graph, {
'/': HomeComponent,
'/about': AboutComponent,
'*': NotFoundComponent,
});
router.navigate('/about');
router.navigate('/about', true); // replaceState instead of pushState
const CurrentPage = router.currentComponent();
const unsub = router.subscribe((path) => console.log('navigated to', path));
const href = router.href('/about'); // returns the path string (identity in v0.1)
The router also listens for popstate events (browser back/forward navigation) and activates the appropriate route node automatically.
7. Compilation Pipeline
7.1 Overview
source.el → [Lexer] → [Parser] → [Codegen] → output
The output format depends on the compilation target (see §7.4).
7.2 Lexer
The lexer (src/lexer.rs) is a single-pass O(n) tokenizer. It is context-sensitive: when it encounters the template keyword followed by {, it switches to template mode, producing template-specific tokens:
| Token | Description |
|---|---|
HashIdent(kw) |
{#if}, {#each}, {#activate} — block open |
SlashIdent(kw) |
{/if}, {/each}, {/activate} — block close |
ColonIdent(kw) |
{:else} — block continuation |
OnColon(event) |
on:click, on:input — event binding |
SelfClose |
/> |
CloseTag(name) |
</div> |
RawText(s) |
Inline expression text, text content |
Non-template code tokens include keywords (component, props, state, fn, template, if, else, return), identifiers, literals (String, Int, Float, Bool), operators, and punctuation.
7.3 Parser
Hand-written recursive descent parser. Produces Vec<Component>. Each component is parsed as:
component Name {— component declarationprops { ... }— prop definitions (optional)state { ... }— state definitions (optional)fn method(...) -> Type { ... }— methods (optional, multiple allowed)template { ... }— template tree (optional)
The template parses as a tree of TemplateNode values:
| Variant | Description |
|---|---|
Element { tag, attrs, children } |
HTML element with attributes and children |
Component { name, props } |
Uppercase-initial component reference |
Text(s) |
Literal text content |
Interpolation(expr) |
{expr} — expression interpolation |
If { condition, then, else_ } |
{#if}...{:else}...{/if} |
Each { items, item_name, children } |
{#each items as item}...{/each} |
Activate { query, result_name, children } |
{#activate "query" as name}...{/activate} |
Method bodies are captured as raw source text and passed through to the code generator with simple string transformations.
Unknown {#blockname} tags produce a parse error (unknown block tag: #name).
7.4 Code Generator — Multi-Target
The code generator (src/codegen.rs) supports three compilation targets:
CodegenTarget::Web (default) — Legacy JavaScript
Emits an ES2022 JavaScript module using the el-ui JS runtime (graph.js, renderer.js, router.js, index.js). This is the primary target for browser-based applications today.
For each component:
- Emits a class extending
Componentfrom the runtime. - Constructor: seeds state nodes into
this._graph, sets prop values asthis._props_{name}, subscribes to activation events. - Emits
setState(name, value)which callsthis._graph.update()to trigger activation. - Emits
render()returning a template literal string. - Translates state assignments:
count = count + 1→__self.setState('count', count + 1). - Translates template interpolations:
{count}→${count }. - Emits event handlers as
data-el-{event}attributes for the renderer to bind. - Emits
${__self._child(ComponentClass, props)}for component references.
CodegenTarget::Server — Node.js SSR
Emits a Node.js CommonJS module (the "render script") that exports renderToString(componentName, props) and renderToDocument(componentName, props, shellOpts). The render script uses runtime/src/ssr.js to parse the component source at module load time and render to HTML strings at call time.
The build_node_tree() / template-to-HTML pipeline is implemented in runtime/src/ssr.js. It covers all TemplateNode variants: Element, Text, Interpolation, If, Each, Activate, Component, and RawHtml. The {#activate} block renders empty on the server (no Engram server available at SSR time); the client runtime re-runs semantic queries after hydration.
The ServerBackend target is the only target where hydration markers are injected: data-el-component, data-el-id, and data-el-state on each component root element.
CodegenTarget::Native(Platform) — Rust Native / WASM
Emits a Rust module where each component builds a PlatformNode tree via the semantic primitive layer and mounts it via the el_platform backend for the given platform.
Supported platforms:
| Platform | Backend | Output format |
|---|---|---|
Platform::Web |
WebBackend (web-sys) |
Rust compiled to WASM — NOT JavaScript |
Platform::Ios |
IosBackend |
Rust (objc2-ui-kit) |
Platform::Android |
AndroidBackend |
Kotlin (Jetpack Compose via build.rs) |
Platform::Macos |
MacosBackend |
Rust (objc2-app-kit) |
Platform::Linux |
LinuxBackend |
Rust (gtk4-rs) |
Platform::Windows |
WindowsBackend |
Rust (windows-rs / WinUI 3) |
Note: The build_node_tree() implementation in all native targets is currently a stub returning PlatformNode::element("div"). Full AST-to-semantic-primitive lowering is not yet wired in — the semantic module and per-platform codegen functions exist and are correct, but the compiler does not yet call them from the AST walk.
7.5 Semantic Primitive Layer (semantic.rs)
The semantic layer is the bridge between the template AST and platform-specific code generation. It defines EBD-driven concepts — what something is, not how it looks:
AppearanceConcept — what a control is:
Action— primary action trigger (blue/filled on most platforms)Destructive— dangerous/irreversible action (red tint)Secondary— subdued / less-prominent actionNavigation— link-like / back-navigation elementInformational— read-only display elementStructural— layout container with no interactive meaning
LayoutConcept — how children are arranged:
Stack— vertical (Column / VStack / GtkBox vertical)Row— horizontal (Row / HStack / GtkBox horizontal)Grid— two-dimensional gridOverlay— z-axis layering (ZStack / GtkOverlay)Scroll— scrollable container
TextConcept — the semantic role of text:
Heading { level: u8 }— heading at level 1–6Body— default body textCaption— small secondary textLabel— control labelCode— monospaced code text
SemanticPrimitive — the EBD building blocks:
Button { label, appearance, on_press }— interactive buttonText { content, concept }— text displayContainer { children, layout }— layout containerInput { binding, hint, appearance }— text input bound to stateImage { src, alt }— image elementToggle { binding, label }— checkbox/switch bound to boolean stateList { items_binding, item_name, item_template }— homogeneous list
Appearance is inferred from explicit appearance="..." attributes, CSS class names, or HTML tag heuristics via infer_appearance(). Layout and text concepts are inferred from tag names via infer_layout_concept() and infer_text_concept().
7.6 CLI
The elc-ui Node.js script in vessels/el-ui-compiler/elc-ui exposes the compiler as a command-line tool:
elc-ui App.el # compile to App.js (web target, default)
elc-ui App.el -o app.js # explicit output path
elc-ui App.el --target=server # emit Node.js render script
elc-ui App.el --target=server -o render.js
elc-ui App.el --runtime=../el-ui.js # custom runtime path for web target
Web target emits an ES2022 JavaScript module — the same class-per-component format as the hand-compiled examples in examples/counter/app.js. Requires the el-ui runtime (el-ui.js or dist/el-ui.js) to be available at the path specified by --runtime.
Server target emits a Node.js CJS module that:
- Embeds the component source at compile time (base64-encoded)
- Exports
renderToString(componentName, props)→ HTML string - Exports
renderToDocument(componentName, props, shellOpts)→ full HTML document - Exports
componentNames→ array of component names found in the source - Supports CLI invocation:
node render.js ComponentName '{"prop":"val"}'
The server render script is self-contained: it does not require re-reading the .el source file at render time. The El web server calls it via exec() and captures stdout as the rendered HTML string.
8. Runtime Architecture
8.1 Module Structure
| Module | Purpose |
|---|---|
graph.js |
Engram graph: nodes, edges, activation, search, subscribe, dump |
activation.js |
Standalone activation utilities: spreadActivation, activationStrength, reachableNodes |
renderer.js |
DOM patching, event binding |
router.js |
Graph-based routing |
index.js |
Component base class, mount(), hydrate(), re-exports |
ssr.js |
Server-side rendering engine (Node.js only, not a browser module) |
8.2 Component Lifecycle
Client-side (mount path):
| Lifecycle event | Description |
|---|---|
constructor(props, initialState?) |
Seeds state graph, subscribes to activation. initialState is used by hydrate() to pre-load server state. |
render() |
Returns HTML string; called on activation |
onMount() |
Called after first render and DOM attachment |
SSR + hydration path:
| Lifecycle event | Description |
|---|---|
constructor(props, initialState) |
Called by hydrate() with recovered server state |
_hydrate(root) |
Subscribes state nodes, creates renderer pointing at existing DOM, binds events. Does NOT call render(). |
onHydrate() |
Called after hydration completes |
Note: onDestroy() is documented in prior drafts but is not implemented in the base Component class or the Renderer. It will not be called by the runtime.
8.3 Mounting
import { mount } from './el-ui.js';
import { App } from './app.js';
const component = mount(App, '#app', { optionalProp: 'value' });
// mount() returns the live Component instance
mount instantiates the root component, creates a Renderer, calls renderer.mount() which calls render(), sets root.innerHTML, binds events, then calls onMount().
8.5 Hydration
import { hydrate } from './el-ui.js';
import { Counter } from './app.js';
// Hydrate a server-rendered Counter without replacing the DOM
const component = hydrate('[data-el-component="Counter"]', Counter);
// or with a stable ID for multi-instance pages:
const component = hydrate('[data-el-id="el-counter-1"]', Counter);
hydrate finds the SSR root element, reads data-el-state to recover the server's initial state, constructs the component with that state pre-loaded, then calls _hydrate(root) which binds all event handlers without re-rendering. The existing server-rendered DOM is preserved; spreading activation begins on the first setState() call.
SSR lifecycle:
Server: renderToString(src, 'App', props) → HTML with markers
Network: HTML delivered, painted immediately (zero JS latency)
Client: import { hydrate } from './el-ui.js'
hydrate('[data-el-component="App"]', App)
→ events bound, spreading activation active
→ subsequent setState() calls patch the DOM normally
8.6 Server-Side Rendering
The SSR engine in runtime/src/ssr.js is a Node.js module that parses .el source files and renders components to HTML strings:
const { renderToString, renderToDocument, parseComponents } = require('./runtime/src/ssr.js');
const fs = require('fs');
const src = fs.readFileSync('App.el', 'utf8');
// Render to HTML fragment
const html = renderToString(src, 'App', { title: 'Hello' });
// Render to full document
const doc = renderToDocument(src, 'App', { title: 'Hello' }, {
title: 'My App',
appScriptPath: './hydrate.js',
styles: 'body { background: #000; }',
});
Hydration markers emitted on every component root element:
| Attribute | Value | Purpose |
|---|---|---|
data-el-component |
component name | identifies component type for hydrate() |
data-el-id |
stable position-based ID | locates the exact instance when multiple components of the same type exist |
data-el-state |
JSON string | server's initial state snapshot; hydrate() reads this to initialize the Engram graph |
Event bindings are preserved as data-el-* attributes (e.g., data-el-click="() => increment()"). The client hydration pass binds these handlers via new Function('__self', ...) exactly as the standard mount path does.
Server/client bridge:
The El web server calls the render script as a subprocess:
node render.js ComponentName '{"prop":"value"}'
# stdout: the rendered HTML string
The render script is generated by elc-ui --target=server. It embeds the component source at compile time, so no .el file is needed at render time.
8.4 DOM Patching Strategy (v0.1)
The renderer uses full string re-render on every state change: root.innerHTML = component.render(). The activated node set argument to patch() is accepted but unused — targeted patching is planned for v0.2.
After patching, the renderer attempts to restore focus to the previously focused element by id.
Event handlers are rebound after every patch by scanning the new DOM for data-el-{event} attributes. Handlers are compiled via new Function('__self', ...) — this requires a permissive Content Security Policy in v0.1.
9. Platform Backend Architecture (el-platform)
The el-platform crate defines the PlatformBackend trait that all rendering backends implement:
pub trait PlatformBackend: Send + Sync {
fn name(&self) -> &'static str;
fn create_element(&self, tag: &str) -> PlatformResult<PlatformNode>;
fn create_text(&self, content: &str) -> PlatformResult<PlatformNode>;
fn set_attribute(&self, node: &mut PlatformNode, name: &str, value: &str) -> PlatformResult<()>;
fn remove_attribute(&self, node: &mut PlatformNode, name: &str) -> PlatformResult<()>;
fn append_child(&self, parent: &mut PlatformNode, child: PlatformNode) -> PlatformResult<()>;
fn remove_child(&self, parent: &mut PlatformNode, child_index: usize) -> PlatformResult<()>;
fn replace_child(&self, parent: &mut PlatformNode, index: usize, new_child: PlatformNode) -> PlatformResult<()>;
fn bind_event(&self, node: &mut PlatformNode, event: &str, handler: EventHandler) -> PlatformResult<()>;
fn render_to_string(&self, node: &PlatformNode) -> PlatformResult<String>;
fn mount(&self, root: PlatformNode, container_id: &str) -> PlatformResult<()>;
fn patch(&self, old: &PlatformNode, new: &PlatformNode) -> PlatformResult<()>;
fn supports_ssr(&self) -> bool { false }
}
The ServerBackend is the only backend where supports_ssr() returns true. In v0.1, the server rendering implementation is in runtime/src/ssr.js (Node.js), not in a Rust crate. The PlatformBackend interface above describes the planned Rust abstraction; the current implementation fulfills the same contract via the JavaScript SSR engine, which is the authoritative server rendering path. All other backends are platform-native.
Platform is selected via manifest.el:
[platform]
target = "web" # web | server | ios | android | macos | linux | windows
ssr = true
10. Comparison to Other Frameworks
10.1 vs. React
React uses virtual DOM diffing: on every render, it constructs a virtual tree and diffs it against the previous tree to identify minimal DOM mutations. The unit of re-render is the component; re-render triggers are driven by setState, useReducer, or context changes.
el-ui does not compute a virtual DOM. The spreading activation pass determines the re-render set from the graph topology. State relationships that exist in the graph are automatically propagated; relationships that do not exist are not. There is no need to declare dependencies, use useMemo, or prevent unnecessary renders with React.memo — the graph structure encodes the dependency information directly.
10.2 vs. Vue
Vue uses a Proxy-based reactive system: state values are wrapped in reactive proxies, and reads from these proxies during render are tracked as dependencies. When a reactive value changes, all components that read it during their last render are re-rendered.
el-ui's graph model is explicit: relationships between state nodes are declared via graph edges, not inferred from read tracking. This makes dependencies inspectable and modifiable at runtime. It also enables semantic queries ({#activate}) that find relevant state by meaning rather than by structural reference.
10.3 vs. Svelte
Svelte uses compile-time analysis to determine which parts of the DOM update when which state changes. The compiler emits targeted DOM mutations. There is no runtime overhead for a virtual DOM or a reactive system.
el-ui uses a runtime spreading activation pass. This is more expensive than Svelte's compile-time approach for small, known-static state graphs. The advantage is dynamic state topology: new nodes and edges can be added at runtime without recompilation, enabling state graphs that evolve with program execution.
10.4 Core Differentiator
No prior framework uses Hebbian spreading activation as the re-render decision mechanism. The multiplicative activation formula, the self-seeded activation from a root node, the importance-boost on state change (LTP analog), and the semantic {#activate} query construct have no analogues in any shipping frontend framework as of April 2026.
11. SSR + Hydration Example
A complete SSR + hydration example lives in examples/ssr-counter/.
11.1 Counter.el
The component definition with state, methods, {#if} conditionals, and child composition:
component Counter {
props {
label: String = "Counter"
}
state {
count: Int = 0
history: String = ""
}
fn increment() -> Void {
count = count + 1
history = history + "+" + count + " "
}
template {
<div class="counter">
<h2>{label}</h2>
<span class={count > 0 ? "count positive" : "count zero"}>{count}</span>
<button on:click={() => increment()}>+</button>
{#if history != ""}
<code>{history}</code>
{/if}
</div>
}
}
11.2 Compile
# Server render script (embed source, export renderToString)
elc-ui Counter.el --target=server -o render.js
# Browser module (class-per-component, for hydration)
elc-ui Counter.el --target=web --runtime=../../dist/el-ui.js -o app.js
11.3 Server Render
# Render to HTML fragment
node render.js Counter
# => <div class="counter" data-el-component="Counter" data-el-id="el-counter-1"
# data-el-state='{"count":0,"history":""}'>
# <h2>Counter</h2>
# <span class="count zero">0</span>
# <button data-el-click="() => increment()">+</button>
# <p class="hint">Press + or - to begin.</p>
# </div>
Or from Node.js:
const { renderToString } = require('./render.js');
const html = renderToString('Counter', { label: 'Score' });
11.4 Client Hydration
// hydrate.js — runs in browser after SSR HTML is painted
import { hydrate } from '../../dist/el-ui.js';
import { Counter } from './app.js';
// Find each server-rendered Counter and attach the runtime
document.querySelectorAll('[data-el-component="Counter"]').forEach(el => {
if (el.dataset.elHydrated) return;
hydrate(`[data-el-id="${el.dataset.elId}"]`, Counter);
el.dataset.elHydrated = 'true';
});
11.5 Full Lifecycle
1. Server calls: node render.js App
2. Server injects: rendered HTML into <div id="app">
3. Browser: HTML painted immediately (no JS needed for first paint)
4. Browser: <script type="module" src="./hydrate.js"> executes
5. hydrate.js: reads data-el-state, constructs Counter with server state
6. hydrate.js: binds data-el-click handlers without re-rendering DOM
7. User clicks +: count = count + 1 → setState → spreading activation → patch
The DOM is never blank. The page is interactive as soon as hydrate.js finishes executing. State is continuous across the SSR/hydration boundary because data-el-state carries the exact initial values the server used.
12. Versioning Roadmap
| Version | Key changes |
|---|---|
| v0.1.x | Current. Full re-render on state change. String-based {#activate} search. Web and Server targets via elc-ui CLI. SSR engine implemented (runtime/src/ssr.js). hydrate() function in client runtime. Native targets available via library API but build_node_tree() stub. |
| v0.2.x | Targeted DOM patching (only nodes in activation surface). {#activate} with embedding-based semantic search. CLI --target flag for native targets. Full AST→semantic→PlatformNode lowering. |
| v0.3.x | WASM-compiled Engram core replacing JavaScript graph. Sealed artifact support. onDestroy() lifecycle. |
| v1.0.0 | Stable API. Full production sealing. LSP integration with spreading activation autocomplete. |