All files / lib/core/runtime AvenxPage.js

94.23% Statements 98/104
78.94% Branches 15/19
100% Functions 5/5
94.23% Lines 98/104

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 1068x 8x 8x 8x 8x 8x 8x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 17x 17x 17x 6x 6x 6x 6x 6x 2x 1x 1x 1x 1x 2x 2x 2x 6x 6x 6x 6x 6x 6x 17x 17x 17x 17x 17x 17x 17x 17x 6x 1x 1x 1x 1x 1x 6x 17x 17x 17x 11x 11x 11x 11x 11x 11x 11x 15x 4x 4x 4x 4x     4x 15x 11x 11x 5x 5x 5x 5x     11x 6x 6x 6x 6x 11x     17x 17x 6x    
import { AvenxComponent } from './AvenxComponent.js';
 
/**
 * AvenxPage is a specialized component that can host child components.
 * It automatically mounts child components defined in its template via [data-avenx-comp].
 */
export class AvenxPage extends AvenxComponent {
    /** @type {Map<string, typeof AvenxComponent>} @private */
    #componentRegistry;
    /** @type {Map<Element, AvenxComponent>} @private */
    #childComponents = new Map();
 
    /**
     * @param {Object} initialState - Initial state.
     * @param {Object} computed - Computed properties.
     * @param {Object} bridges - Shared bridges.
     * @param {string} template - HTML template.
     * @param {Object} methods - Component methods.
     * @param {Map<string, typeof AvenxComponent>} componentRegistry - Registry of available components.
     */
    constructor(initialState = {}, computed = {}, bridges = {}, template = '', methods = {}, componentRegistry = new Map(), props = {}) {
        super(initialState, computed, bridges, template, methods, props);
        this.#componentRegistry = componentRegistry;
    }
 
    /**
     * Updates the page and then mounts/updates child components.
     */
    update() {
        super.update();
        this.#mountChildComponents();
    }
 
    /**
     * Unmounts the page and all child components.
     */
    unmount() {
        for (const compInstance of this.#childComponents.values()) {
            if (typeof compInstance.unmount === 'function') {
                compInstance.unmount();
            }
        }
        this.#childComponents.clear();
        super.unmount();
    }
 
    /**
     * Finds all mount points for child components and initializes or updates them.
     * @private
     */
    #mountChildComponents() {
        const root = this._getElement();
        if (!root) return;
 
        const mountPoints = root.querySelectorAll('[data-avenx-comp]');
        const currentElements = new Set(mountPoints);
 
        // 1. Clean up/unmount child components whose elements are no longer in the DOM/page
        for (const [el, compInstance] of this.#childComponents.entries()) {
            if (!currentElements.has(el) || !root.contains(el)) {
                if (typeof compInstance.unmount === 'function') {
                    compInstance.unmount();
                }
                this.#childComponents.delete(el);
            }
        }
 
        // 2. Instantiate new components or update existing ones
        mountPoints.forEach(el => {
            const compName = el.getAttribute('data-avenx-comp');
            const CompClass = this.#componentRegistry.get(compName);
            
            if (CompClass) {
                // Extract props from element's data-props-* attributes
                const props = {};
                for (const attr of el.attributes) {
                    if (attr.name.startsWith('data-props-')) {
                        const propName = attr.name.slice('data-props-'.length);
                        try {
                            props[propName] = this._evaluate(attr.value);
                        } catch (e) {
                            console.warn(`[AvenxPage] Failed to evaluate prop expression: ${attr.value}`, e);
                        }
                    }
                }
 
                if (this.#childComponents.has(el)) {
                    const compInstance = this.#childComponents.get(el);
                    if (typeof compInstance.setProps === 'function') {
                        compInstance.setProps(props);
                    } else if (typeof compInstance.update === 'function') {
                        compInstance.update();
                    }
                } else {
                    const compInstance = new CompClass(this._getBridges(), props);
                    compInstance.mount(el);
                    this.#childComponents.set(el, compInstance);
                }
            } else {
                console.warn(`[AvenxPage] Component '${compName}' not found in registry.`);
            }
        });
    }
}