Files
vacuum-wall/webui/static/hoover/component.js
T
mteehan 5135de0921 feat: add system config import, refactor install script and nginx auth
- lib/system_import: new module to import system configs into JSON at daemon startup
- daemon/server.py: call import_all() during startup for config reconciliation
- daemon/handlers/nginx.py: simplify add_domain auth handling, remove duplicate code
- scripts/install.sh: replace inline Python setup with curl-based daemon API calls; apply IP forwarding at runtime
- hoover: bump internal asset versions to v=8
- pages: bump asset versions to v=9
2026-07-08 02:20:28 +00:00

119 lines
3.1 KiB
JavaScript

/**
* Hoover — component.js
*
* Component wrapper: definePage, lifecycle hooks, state caching.
*
* definePage wraps a page definition into a renderer function compatible
* with hoover's render engine. Handles reactive state creation and
* lifecycle management. Data loading is handled by the model layer.
*
* Usage:
* export default definePage({
* init() { return { firewall: getModel('firewall') }; },
* async load(state) { ... }, // optional, for one-time setup
* render(state) { return [vnodes],
* });
*/
import { reactive } from './reactivity.js?v=8';
import { h } from './vdom.js?v=8';
import { _compExpandedCache } from './render.js?v=8';
/** Registry of mounted components: key → { state } */
const _mounted = new Map();
/**
* Define a page component.
*
* @param {object} def — Page definition
* @param {function} def.init — Return initial state object
* @param {function} [def.load] — Optional one-time setup called on mount
* @param {function} def.render — Render function that returns vnodes
* @returns {object} — Component renderer compatible with h('#comp', ...)
*/
export function definePage(def) {
let state = null;
let stateInitialized = false;
const renderer = () => {
if (!stateInitialized) {
state = reactive(def.init());
stateInitialized = true;
}
return def.render(state);
};
renderer._pageDef = {
get state() {
if (!stateInitialized) {
state = reactive(def.init());
stateInitialized = true;
}
return state;
},
load: def.load || null,
onUnmount: def.onUnmount || null,
};
return renderer;
}
/**
* Mount a page component. Called by the render engine when a #comp vnode
* enters the tree for the first time.
*/
export function mountComponent(key, renderer) {
const pd = renderer._pageDef;
if (!pd) return;
let entry = _mounted.get(key);
if (entry) {
// Re-mount: component already exists with its state.
// Don't re-run load — that re-render was triggered by a reactive update.
return;
}
entry = { state: pd.state };
_mounted.set(key, entry);
pd.state.error = null;
if (pd.load) {
Promise.resolve().then(() => pd.load(pd.state));
}
}
/**
* Unmount a page component. Called by the render engine when a #comp vnode
* is removed from the tree.
*/
export function unmountComponent(key, renderer) {
const entry = _mounted.get(key);
if (!entry) return;
const pd = renderer._pageDef;
if (pd.onUnmount) {
try { pd.onUnmount(entry.state); } catch (_) {}
}
_compExpandedCache.delete(key);
_mounted.delete(key);
}
/**
* Get the state of a mounted component.
*/
export function getComponentState(key) {
const entry = _mounted.get(key);
return entry ? entry.state : null;
}
/**
* Create a component vnode that the render engine will wire up to lifecycle.
*/
export function hComp(renderer, key) {
return h('#comp', { component: renderer, key }, []);
}