Source: /ui/componentsbaseComponent.mjs

import van from "../vanjs.mjs";

const HtmlRenderer = (htmlString) => {
    const container = van.tags.div(); // Create a container div
    container.innerHTML = htmlString; // Set innerHTML to render safely
    return container;
};

/**
 * @class BaseComponent
 * @description The base class for all components
 */
class BaseComponent {

    /**
     *
     * @param {*} options - other options are defined in the constructor of the derived class
     * @param  {...any} args
     * @param {string} [options.id] - The id of the component
     */
    constructor(options, ...args) {
        const extraClasses = options["extraClass"];
        this.classMap = typeof extraClasses === "object" ? extraClasses : {};
        this.additionalProperties = options["additionalProperties"] || {};
        this._children = args;
        this._renderedChildren = null;
        this._initializing = true;
        this.classState = van.state("");

        if (options) {
            if (options.id) {
                this.id = options.id;
                delete options.id;
            }
            this.options = options;
        } else {
            this.options = {};
        }
    }

    /**
     *
     * @param {*} element - The element to attach the component to
     */
    attachTo(element) {
        this._initializing = false;
        this.refreshState();
        if (element instanceof BaseComponent) {
            element.addChildren(this);
        } else {
            van.add(element,
                this.create());
        }
    }

    /**
     * Refresh the state of the component, e.g. class names
     */
    refreshState() {
        this.classState.val = Object.values(this.classMap).join(" ");
    }

    /**
     *
     * @param  {...any} properties - functions to set the state of the component
     */
    set(...properties) {
        for (let property of properties) {
            property.call(this);
        }
    }
    /**
     *
     * @param  {...any} children - children to add to the component
     */
    addChildren(...children) {
        this._children.push(...children);
    }

    /**
     * getter for children which will automatically refresh them and create them if they are BaseComponent
     */
    get children() {
        if (this._renderedChildren) return this._renderedChildren;
        this._renderedChildren = (this._children || []).map(child => {
            if (child instanceof BaseComponent) {
                child.refreshState();
                return child.create();
            }
            if (child instanceof Element) {
                return child;
            }
            if (typeof child === "string") {
                return child.trimStart().startsWith("<") ? HtmlRenderer(child) : child;
            }
            console.warn(`Invalid child component provided - ${typeof child}:`, child);
            return undefined;
        }).filter(Boolean);
        return this._renderedChildren;
    }

    /**
     * getter for commonProperties which are shared against all components
     */
    get commonProperties() {
        if (this.id) {
            return {
                id: this.id,
                class: this.classState
            };
        };

        return {
            class: this.classState
        };
    }

    /**
     *
     * @param {string} key - The key of the class
     * @param {string} value - The value of the class
     * @description Set the class of the component
     * @example
     * button.setClass("size", "btn-lg");
     */
    setClass(key, value) {
        this.classMap[key] = value;
        if (!this._initializing) {
            this.classState.val = Object.values(this.classMap).join(" ");
        }
    }

    /**
     * @description Create the component
     * it needs to be overridden by the derived class
     */
    create() {
        throw new Error("Component must override create method");
    }

    /**
     * If you document a component properties like this:
     * Component.PROPERTY = {
     *     X: function () { ... do something ... },
     *     Y: function () { ... do something ... },
     * };
     * You can use this function that will iterate options object
     * and for each component, calls the initialization where necessary.
     *
     * Usage (in constructor): this._applyOptions(options, "X", "Y");
     *
     * @param options
     * @param {string} names keys to the options object, values of the keys
     * should be functions
     */
    _applyOptions(options, ...names) {
        const wasInitializing = this._initializing;
        this._initializing = true;
        for (let prop of names) {
            const option = options[prop];
            try {
                if (option) option.call(this);
            } catch (e) {
                console.warn("Probably incorrect component usage! Option values should be component-defined functional properties!", e);
            }
        }
        this._initializing = wasInitializing;
        if (wasInitializing) {
            this.refreshState();
        }

    }
}

export { BaseComponent };