feat: el-ui — activation-based frontend framework, spreading activation reactivity, graph state
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* el-ui — Activation-based frontend framework.
|
||||
*
|
||||
* State is an Engram graph. Reactivity is spreading activation.
|
||||
*
|
||||
* Core exports:
|
||||
* Graph — in-browser Engram graph (nodes, edges, activation)
|
||||
* Renderer — DOM mounting and patching
|
||||
* Router — graph-based routing
|
||||
* Component — base class for el-ui components
|
||||
* mount() — attach a component to the DOM
|
||||
*
|
||||
* @module el-ui
|
||||
*/
|
||||
|
||||
export { Graph } from './graph.js';
|
||||
export { Renderer } from './renderer.js';
|
||||
export { Router } from './router.js';
|
||||
export {
|
||||
spreadActivation,
|
||||
activationStrength,
|
||||
reachableNodes,
|
||||
PRUNE_THRESHOLD,
|
||||
} from './activation.js';
|
||||
|
||||
import { Graph } from './graph.js';
|
||||
import { Renderer } from './renderer.js';
|
||||
|
||||
/**
|
||||
* Base class for all el-ui components.
|
||||
*
|
||||
* Subclasses override:
|
||||
* render() → returns HTML string
|
||||
* onMount() → called after first mount (optional)
|
||||
*
|
||||
* Subclasses call:
|
||||
* setState(name, value) → update state, trigger activation, patch DOM
|
||||
*/
|
||||
export class Component {
|
||||
constructor(props = {}) {
|
||||
/** @type {Graph} */
|
||||
this._graph = new Graph();
|
||||
|
||||
/** @type {Record<string, string>} state node IDs */
|
||||
this._stateNodes = {};
|
||||
|
||||
/** @type {Record<string, any>} current state values */
|
||||
this._state = {};
|
||||
|
||||
/** @type {Record<string, any>} current prop values */
|
||||
this.props = props;
|
||||
|
||||
/** @type {Renderer|null} */
|
||||
this._renderer = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a named state variable.
|
||||
* Triggers spreading activation in the graph, then patches the DOM.
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {any} value
|
||||
*/
|
||||
setState(name, value) {
|
||||
if (this._stateNodes[name] !== undefined) {
|
||||
this._graph.update(this._stateNodes[name], value);
|
||||
// Graph.update() notifies subscribers, which call _renderer.patch().
|
||||
// Belt-and-suspenders: also patch directly in case no subscribers yet.
|
||||
if (this._renderer) this._renderer.patch();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a child component inline, sharing the parent's activation graph.
|
||||
* Used by the compiler to embed child components in template strings.
|
||||
*
|
||||
* @param {typeof Component} ComponentClass
|
||||
* @param {Record<string, any>} props
|
||||
* @returns {string}
|
||||
*/
|
||||
_child(ComponentClass, props = {}) {
|
||||
try {
|
||||
const instance = new ComponentClass(props);
|
||||
// Share the parent graph so child state participates in the same
|
||||
// activation surface. Child state changes can propagate upward.
|
||||
instance._graph = this._graph;
|
||||
return instance.render();
|
||||
} catch (e) {
|
||||
console.warn(`el-ui: failed to render child ${ComponentClass?.name}`, e);
|
||||
return `<!-- el-ui: render error in ${ComponentClass?.name} -->`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to return the component's HTML string.
|
||||
* All state and props are accessible via this._state / this.props.
|
||||
* @returns {string}
|
||||
*/
|
||||
render() {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Called once after the component is first mounted to the DOM.
|
||||
* Override for side effects (timers, fetch calls, subscriptions).
|
||||
*/
|
||||
onMount() {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount a component to a DOM element and start the el-ui runtime.
|
||||
*
|
||||
* @param {typeof Component} ComponentClass the component class to mount
|
||||
* @param {string} selector CSS selector for the root element
|
||||
* @param {Record<string, any>} [props={}] initial props
|
||||
* @returns {Component} the live component instance
|
||||
*
|
||||
* @example
|
||||
* import { mount } from './el-ui.js';
|
||||
* import { Counter } from './counter.js';
|
||||
* mount(Counter, '#app');
|
||||
*/
|
||||
export function mount(ComponentClass, selector, props = {}) {
|
||||
const root = document.querySelector(selector);
|
||||
if (!root) {
|
||||
throw new Error(`el-ui: no element found for selector '${selector}'`);
|
||||
}
|
||||
|
||||
const component = new ComponentClass(props);
|
||||
const renderer = new Renderer(root, component);
|
||||
component._renderer = renderer;
|
||||
renderer.mount();
|
||||
return component;
|
||||
}
|
||||
|
||||
export default { mount, Component, Graph, Renderer };
|
||||
Reference in New Issue
Block a user