b673e87c9b
Add hoover model.js as a central reactive store per subsystem, replacing per-component data fetching with a single source of truth. - Add hoover/model.js with modelRegister, modelFetch, and WS invalidation - Refactor websocket.js to route messages to model refresh (drop per-component subscribe/unsubscribe) - Simplify component.js by removing WS subscription management - Add refresh option to apiSubmit, deprecate refactorLoad and checkAbort - Rewrite all pages to use getModel() instead of inline data fetching - Bootstrap model registrations in app.js - Add GET /api/firewall/state endpoint - Fix restart-services.sh restart order and add service health verification - Update hoover.md docs with model layer architecture
119 lines
3.1 KiB
JavaScript
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=7';
|
|
import { h } from './vdom.js?v=7';
|
|
import { _compExpandedCache } from './render.js?v=7';
|
|
|
|
/** 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 }, []);
|
|
}
|