Home Manual Reference Source

source/widget/WidgetRegistrySingleton.js

import makeCustomError from "../makeCustomError";

/**
 * The instance of the {@link WidgetRegistrySingleton}.
 */
let _instance = null;


/**
 * Exception thrown when an element where we expect the
 * ``data-ievv-jsbase-widget-instanceid`` attribute does
 * not have this attribute.
 *
 * @type {Error}
 */
export let ElementHasNoWidgetInstanceIdError = makeCustomError('ElementHasNoWidgetInstanceIdError');


/**
 * Exception thrown when an element that we expect to have
 * the ``data-ievv-jsbase-widget`` attribute does not have
 * this attribute.
 *
 * @type {Error}
 */
export let ElementIsNotWidgetError = makeCustomError('ElementIsNotWidgetError');


/**
 * Exception thrown when an element has a
 * ``data-ievv-jsbase-widget`` with a value that
 * is not an alias registered in the {@link WidgetRegistrySingleton}.
 *
 * @type {Error}
 */
export let InvalidWidgetAliasError = makeCustomError('InvalidWidgetAliasError');


/**
 * Exception thrown when an element with the
 * ``data-ievv-jsbase-widget-instanceid=<widgetInstanceId>`` attribute is not in
 * the {@link WidgetRegistrySingleton} with ``<widgetInstanceId>``.
 *
 * @type {Error}
 */
export let ElementIsNotInitializedAsWidget = makeCustomError('ElementIsNotInitializedAsWidget');


/**
 * A very lightweight widget system.
 *
 * Basic example below - see {@link AbstractWidget} for more examples.
 *
 * @example <caption>Create a very simple widget</caption>
 * export default class OpenMenuWidget extends AbstractWidget {
 *     constructor(element) {
 *          super(element);
 *          this._onClickBound = (...args) => {
 *              this._onClick(...args);
 *          };
 *          this.element.addEventListener('click', this._onClickBound);
 *     }
 *
 *     _onClick = (e) => {
 *          e.preventDefault();
 *          console.log('I should have opened the menu here');
 *     }
 *
 *     destroy() {
 *          this.element.removeEventListener('click', this._onClickBound);
 *     }
 * }
 *
 * @example <caption>Use the widget</caption>
 * <button data-ievv-jsbase-widget="open-menu-button" type="button">
 *     Open menu
 * </button>
 *
 * @example <caption>Register and load widgets</caption>
 * // Somewhere that is called after all the widgets are rendered
 * // - typically at the end of the <body>
 * import WidgetRegistrySingleton from 'ievv_jsbase/widget/WidgetRegistrySingleton';
 * import OpenMenuWidget from 'path/to/OpenMenuWidget';
 * const widgetRegistry = new WidgetRegistrySingleton();
 * widgetRegistry.registerWidgetClass('open-menu-button', OpenMenuWidget);
 * widgetRegistry.initializeAllWidgetsWithinElement(document.body);
 *
 */
export default class WidgetRegistrySingleton {
    constructor() {
        if (!_instance) {
            _instance = this;
            this._initialize();
        }
        return _instance;
    }

    _initialize() {
        this._widgetAttribute = 'data-ievv-jsbase-widget';
        this._widgetInstanceIdAttribute = 'data-ievv-jsbase-widget-instanceid';
        this._widgetClassMap = new Map();
        this._widgetInstanceMap = new Map();
        this._widgetInstanceCounter = 0;
    }

    clear() {
        // TODO: Call destroyAllWidgetsWithinDocumentBody()
        this._widgetClassMap.clear();
        this._widgetInstanceMap.clear();
        this._widgetInstanceCounter = 0;
    }

    /**
     * Register a widget class in the registry.
     *
     * @param {string} alias The alias for the widget. This is the string that
     *      is used as the attribute value with the ``data-ievv-jsbase-widget``
     *      DOM element attribute.
     * @param {AbstractWidget} WidgetClass The widget class.
     */
    registerWidgetClass(alias, WidgetClass) {
        this._widgetClassMap.set(alias, WidgetClass);
    }

    /**
     * Remove widget class from registry.
     *
     * @param alias The alias that the widget class was registered with
     *      by using {@link WidgetRegistrySingleton#registerWidgetClass}.
     */
    removeWidgetClass(alias) {
        this._widgetClassMap.delete(alias);
    }

    /**
     * Initialize the provided element as a widget.
     *
     * @param {Element} element The DOM element to initalize as a widget.
     *
     * @throws {ElementIsNotWidgetError} If the element does not have
     *      the ``data-ievv-jsbase-widget`` attribute.
     * @throws {InvalidWidgetAliasError} If the widget alias is not in this registry.
     */
    initializeWidget(element) {
        let alias = element.getAttribute(this._widgetAttribute);
        if(!alias) {
            throw new ElementIsNotWidgetError(
                `The\n\n${element.outerHTML}\n\nelement has no or empty` +
                `${this._widgetAttribute} attribute.`);
        }
        if(!this._widgetClassMap.has(alias)) {
            throw new InvalidWidgetAliasError(`No WidgetClass registered with the "${alias}" alias.`);
        }
        let WidgetClass = this._widgetClassMap.get(alias);
        this._widgetInstanceCounter ++;
        let widgetInstanceId = this._widgetInstanceCounter.toString();
        let widget = new WidgetClass(element, widgetInstanceId);
        this._widgetInstanceMap.set(widgetInstanceId, widget);
        element.setAttribute(this._widgetInstanceIdAttribute, widgetInstanceId);
        return widget;
    }

    _getAllWidgetElementsWithinElement(element) {
        return Array.from(element.querySelectorAll(`[${this._widgetAttribute}]`));
    }

    /**
     * Initialize all widgets within the provided element.
     *
     * @param {Element} element A DOM element.
     */
    initializeAllWidgetsWithinElement(element) {
        const afterInitializeAllWidgets = [];
        for(let widgetElement of this._getAllWidgetElementsWithinElement(element)) {
            let widget = this.initializeWidget(widgetElement);
            if(widget.useAfterInitializeAllWidgets()) {
                afterInitializeAllWidgets.push(widget);
            }
        }
        for(let widget of afterInitializeAllWidgets) {
            widget.afterInitializeAllWidgets();
        }
    }

    /**
     * Get the value of the ``data-ievv-jsbase-widget-instanceid`` attribute
     * of the provided element.
     *
     * @param {Element} element A DOM element.
     * @returns {null|string}
     */
    getWidgetInstanceIdFromElement(element) {
        return element.getAttribute(this._widgetInstanceIdAttribute);
    }

    /**
     * Get a widget instance by its widget instance id.
     *
     * @param widgetInstanceId A widget instance id.
     * @returns {AbstractWidget} A widget instance or ``null``.
     */
    getWidgetInstanceByInstanceId(widgetInstanceId) {
        return this._widgetInstanceMap.get(widgetInstanceId);
    }

    getWidgetInstanceFromElement(element) {
        let widgetInstanceId = this.getWidgetInstanceIdFromElement(element);
        if(widgetInstanceId) {
            let widgetInstance = this.getWidgetInstanceByInstanceId(widgetInstanceId);
            if(widgetInstance) {
                return widgetInstance;
            } else {
                throw new ElementIsNotInitializedAsWidget(
                    `Element\n\n${element.outerHTML}\n\nhas the ` +
                    `${this._widgetInstanceIdAttribute} attribute, but the id is ` +
                    `not in the widget registry.`);
            }
        } else {
            throw new ElementHasNoWidgetInstanceIdError(
                `Element\n\n${element.outerHTML}\n\nhas no or empty ` +
                `${this._widgetInstanceIdAttribute} attribute.`);
        }
    }

    /**
     * Destroy the widget on the provided element.
     *
     * @param {Element} element A DOM element that has been initialized by
     *      {@link WidgetRegistrySingleton#initializeWidget}.
     *
     * @throws {ElementHasNoWidgetInstanceIdError} If the element has
     *      no ``data-ievv-jsbase-widget-instanceid`` attribute or the
     *      attribute value is empty. This normally means that
     *      the element is not a widget, or that the widget
     *      is not initialized.
     * @throws {ElementIsNotInitializedAsWidget} If the element
     *      has the ``data-ievv-jsbase-widget-instanceid`` attribute
     *      but the value of the attribute is not a valid widget instance
     *      id. This should not happen unless you manipulate the
     *      attribute manually or use the private members of this registry.
     */
    destroyWidget(element) {
        let widgetInstanceId = this.getWidgetInstanceIdFromElement(element);
        if(widgetInstanceId) {
            let widgetInstance = this.getWidgetInstanceByInstanceId(widgetInstanceId);
            if(widgetInstance) {
                widgetInstance.destroy();
                this._widgetInstanceMap.delete(widgetInstanceId);
                element.removeAttribute(this._widgetInstanceIdAttribute);
            } else {
                throw new ElementIsNotInitializedAsWidget(
                    `Element\n\n${element.outerHTML}\n\nhas the ` +
                    `${this._widgetInstanceIdAttribute} attribute, but the id is ` +
                    `not in the widget registry.`);
                }
        } else {
            throw new ElementHasNoWidgetInstanceIdError(
                `Element\n\n${element.outerHTML}\n\nhas no or empty ` +
                `${this._widgetInstanceIdAttribute} attribute.`);
        }
    }

    _getAllInstanciatedWidgetElementsWithinElement(element) {
        return Array.from(element.querySelectorAll(`[${this._widgetInstanceIdAttribute}]`));
    }

    /**
     * Destroy all widgets within the provided element.
     * Only destroys widgets on elements that is a child of the element.
     *
     * @param {Element} element The DOM Element.
     */
    destroyAllWidgetsWithinElement(element) {
        for(let widgetElement of this._getAllInstanciatedWidgetElementsWithinElement(element)) {
            this.destroyWidget(widgetElement);
        }
    }
}