Files
el/ui/runtime/src/index.js
T

137 lines
4.1 KiB
JavaScript

/**
* 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 };