UNPKG

canoejs

Version:

A lightweight, widget-based UI framework

161 lines (131 loc) 4.67 kB
import { Canoe } from "../canoe"; export interface WidgetLifecycle { onMount?: () => void; onUnmount?: () => void; onUpdate?: (prevState: any, newState: any) => void; shouldUpdate?: (prevState: any, newState: any) => boolean; } export default abstract class Widget { id: string; protected props: any; protected state: any = {}; protected lifecycle: WidgetLifecycle; protected mounted: boolean = false; protected element: HTMLElement | null = null; protected root: HTMLElement | null = null; private stateSubscriptions: (() => void)[] = []; private stateDependencies: Set<string> = new Set(); constructor(props: any = {}, lifecycle: WidgetLifecycle = {}) { this.props = props; this.lifecycle = lifecycle; this.id = this.generateId(); // Registrar el widget en el sistema de gestión Canoe.registerWidget(this.id, this); } protected update = () => { if (this.root) { const nuevo = this.renderWithDependencyTracking(); (this.root as Element).replaceWith(nuevo as Element); this.root = nuevo as HTMLElement; } }; protected generateId(): string { return `widget_${Math.random().toString(36).substr(2, 9)}`; } // Suscribirse a cambios del estado global protected subscribeToGlobalState(stateKey: string, callback: () => void): void { const unsubscribe = Canoe.subscribeToState(stateKey, callback); this.stateSubscriptions.push(unsubscribe); } // Limpiar todas las suscripciones protected clearSubscriptions(): void { this.stateSubscriptions.forEach(unsubscribe => unsubscribe()); this.stateSubscriptions = []; } protected setState(newState: any): void { const prevState = { ...this.state }; this.state = { ...this.state, ...newState }; if (this.lifecycle.shouldUpdate) { if (!this.lifecycle.shouldUpdate(prevState, this.state)) { return; } } if (this.lifecycle.onUpdate) { this.lifecycle.onUpdate(prevState, this.state); } this.forceUpdate(); } protected forceUpdate(): void { // Marcar el widget para actualización en el sistema global Canoe.markWidgetForUpdate(this.id); // Si el widget está montado, actualizar inmediatamente if (this.mounted && this.element) { const newElement = this.render(); if (newElement && this.element.parentNode) { this.element.parentNode.replaceChild(newElement, this.element); this.element = newElement; } } } mount(): void { if (!this.mounted) { this.mounted = true; if (this.lifecycle.onMount) { this.lifecycle.onMount(); } } } unmount(): void { if (this.mounted) { this.mounted = false; this.clearSubscriptions(); if (this.lifecycle.onUnmount) { this.lifecycle.onUnmount(); } } // Desregistrar el widget del sistema de gestión Canoe.unregisterWidget(this.id); } abstract render(): HTMLElement; // Método que combina render con tracking de dependencias protected renderWithDependencyTracking(): HTMLElement { // Limpiar dependencias anteriores this.stateDependencies.clear(); // Crear un proxy para detectar accesos al estado global const originalGetState = Canoe.getState; const accessedKeys: string[] = []; Canoe.getState = function() { const state = originalGetState.call(this); // Crear un proxy para detectar qué propiedades se acceden return new Proxy(state, { get(target, prop) { if (typeof prop === 'string') { accessedKeys.push(prop); } return target[prop]; } }); }; // Llamar al método render original const result = this.render(); // Restaurar getState original Canoe.getState = originalGetState; // Guardar las dependencias detectadas accessedKeys.forEach(key => this.stateDependencies.add(key)); // Suscribirse a las dependencias detectadas this.subscribeToDetectedDependencies(); return result; } // Obtener las dependencias detectadas protected getStateDependencies(): string[] { return Array.from(this.stateDependencies); } // Suscribirse automáticamente a las dependencias detectadas protected subscribeToDetectedDependencies(): void { this.stateDependencies.forEach(dependency => { this.subscribeToGlobalState(dependency, () => { this.update(); }); }); } }