Skip to content
Snippets Groups Projects
Select Git revision
  • ac97ce5ab2a49171baf844b97b6ddb193d552b8e
  • master default protected
  • 1.31
  • 4.25.2
  • 4.25.1
  • 4.25.0
  • 4.24.3
  • 4.24.2
  • 4.24.1
  • 4.24.0
  • 4.23.6
  • 4.23.5
  • 4.23.4
  • 4.23.3
  • 4.23.2
  • 4.23.1
  • 4.23.0
  • 4.22.3
  • 4.22.2
  • 4.22.1
  • 4.22.0
  • 4.21.0
  • 4.20.1
23 results

buildmap.js

Blame
  • updater.mjs 30.26 KiB
    /**
     * Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
     * Node module: @schukai/monster
     *
     * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
     * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
     *
     * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
     * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
     * For more information about purchasing a commercial license, please contact schukai GmbH.
     *
     * SPDX-License-Identifier: AGPL-3.0
     */
    
    import {internalSymbol} from "../constants.mjs";
    import {diff} from "../data/diff.mjs";
    import {Pathfinder} from "../data/pathfinder.mjs";
    import {Pipe} from "../data/pipe.mjs";
    import {
        ATTRIBUTE_ERRORMESSAGE,
        ATTRIBUTE_UPDATER_ATTRIBUTES,
        ATTRIBUTE_UPDATER_BIND,
        ATTRIBUTE_UPDATER_BIND_TYPE,
        ATTRIBUTE_UPDATER_INSERT,
        ATTRIBUTE_UPDATER_INSERT_REFERENCE,
        ATTRIBUTE_UPDATER_REMOVE,
        ATTRIBUTE_UPDATER_REPLACE,
        ATTRIBUTE_UPDATER_SELECT_THIS,
    } from "./constants.mjs";
    
    import {Base} from "../types/base.mjs";
    import {isArray, isString, isInstance, isIterable} from "../types/is.mjs";
    import {Observer} from "../types/observer.mjs";
    import {ProxyObserver} from "../types/proxyobserver.mjs";
    import {validateArray, validateInstance} from "../types/validate.mjs";
    import {Sleep} from "../util/sleep.mjs";
    import {clone} from "../util/clone.mjs";
    import {trimSpaces} from "../util/trimspaces.mjs";
    import {addAttributeToken, addToObjectLink} from "./attributes.mjs";
    import {updaterTransformerMethodsSymbol} from "./customelement.mjs";
    import {findTargetElementFromEvent} from "./events.mjs";
    import {findDocumentTemplate} from "./template.mjs";
    import {getWindow} from "./util.mjs";
    
    export {Updater, addObjectWithUpdaterToElement};
    
    /**
     * The updater class connects an object with the DOM. In this way, structures and contents in the DOM can be
     * programmatically adapted via attributes.
     *
     * For example, to include a string from an object, the attribute `data-monster-replace` can be used.
     * a further explanation can be found under [monsterjs.org](https://monsterjs.org/)
     *
     * Changes to attributes are made only when the direct values are changed. If you want to assign changes
     * to other values as well, you have to insert the attribute `data-monster-select-this`. This should be
     * done with care, as it can reduce performance.
     *
     * @externalExample ../../example/dom/updater.mjs
     * @license AGPLv3
     * @since 1.8.0
     * @copyright schukai GmbH
     * @memberOf Monster.DOM
     * @throws {Error} the value is not iterable
     * @throws {Error} pipes are not allowed when cloning a node.
     * @throws {Error} no template was found with the specified key.
     * @throws {Error} the maximum depth for the recursion is reached.
     * @throws {TypeError} value is not a object
     * @throws {TypeError} value is not an instance of HTMLElement
     * @summary The updater class connects an object with the dom
     */
    class Updater extends Base {
        /**
         * @since 1.8.0
         * @param {HTMLElement} element
         * @param {object|ProxyObserver|undefined} subject
         * @throws {TypeError} value is not a object
         * @throws {TypeError} value is not an instance of HTMLElement
         * @see {@link Monster.DOM.findDocumentTemplate}
         */
        constructor(element, subject) {
            super();
    
            /**
             * @type {HTMLElement}
             */
            if (subject === undefined) subject = {};
            if (!isInstance(subject, ProxyObserver)) {
                subject = new ProxyObserver(subject);
            }
    
            this[internalSymbol] = {
                element: validateInstance(element, HTMLElement),
                last: {},
                callbacks: new Map(),
                eventTypes: ["keyup", "click", "change", "drop", "touchend", "input"],
                subject: subject,
            };
    
            this[internalSymbol].callbacks.set(
                "checkstate",
                getCheckStateCallback.call(this),
            );
    
            this[internalSymbol].subject.attachObserver(
                new Observer(() => {
                    const s = this[internalSymbol].subject.getRealSubject();
    
                    const diffResult = diff(this[internalSymbol].last, s);
                    this[internalSymbol].last = clone(s);
    
                    const promises = [];
    
                    for (const [, change] of Object.entries(diffResult)) {
                        promises.push(
                            new Promise((resolve, reject) => {
                                getWindow().requestAnimationFrame(() => {
    
                                    try {
    
                                        removeElement.call(this, change);
                                        insertElement.call(this, change);
                                        updateContent.call(this, change);
                                        updateAttributes.call(this, change);
    
                                        resolve();
    
                                    } catch (error) {
                                        reject(error);
                                    }
    
                                });
                            }),
                        );
                    }
    
                    return Promise.all(promises);
                }),
            );
        }
    
        /**
         * Defaults: 'keyup', 'click', 'change', 'drop', 'touchend'
         *
         * @see {@link https://developer.mozilla.org/de/docs/Web/Events}
         * @since 1.9.0
         * @param {Array} types
         * @return {Updater}
         */
        setEventTypes(types) {
            this[internalSymbol].eventTypes = validateArray(types);
            return this;
        }
    
        /**
         * With this method, the eventlisteners are hooked in and the magic begins.
         *
         * ```
         * updater.run().then(() => {
         *   updater.enableEventProcessing();
         * });
         * ```
         *
         * @since 1.9.0
         * @return {Updater}
         * @throws {Error} the bind argument must start as a value with a path
         */
        enableEventProcessing() {
            this.disableEventProcessing();
    
            for (const type of this[internalSymbol].eventTypes) {
                // @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
                this[internalSymbol].element.addEventListener(
                    type,
                    getControlEventHandler.call(this),
                    {
                        capture: true,
                        passive: true,
                    },
                );
            }
    
            return this;
        }
    
        /**
         * This method turns off the magic or who loves it more profane it removes the eventListener.
         *
         * @since 1.9.0
         * @return {Updater}
         */
        disableEventProcessing() {
            for (const type of this[internalSymbol].eventTypes) {
                this[internalSymbol].element.removeEventListener(
                    type,
                    getControlEventHandler.call(this),
                );
            }
    
            return this;
        }
    
        /**
         * The run method must be called for the update to start working.
         * The method ensures that changes are detected.
         *
         * ```
         * updater.run().then(() => {
         *   updater.enableEventProcessing();
         * });
         * ```
         *
         * @summary Let the magic begin
         * @return {Promise}
         */
        run() {
            // the key __init__has no further meaning and is only
            // used to create the diff for empty objects.
            this[internalSymbol].last = {__init__: true};
            return this[internalSymbol].subject.notifyObservers();
        }
    
        /**
         * Gets the values of bound elements and changes them in subject
         *
         * @since 1.27.0
         * @return {Monster.DOM.Updater}
         */
        retrieve() {
            retrieveFromBindings.call(this);
            return this;
        }
    
        /**
         * If you have passed a ProxyObserver in the constructor, you will get the object that the ProxyObserver manages here.
         * However, if you passed a simple object, here you will get a proxy for that object.
         *
         * For changes, the ProxyObserver must be used.
         *
         * @since 1.8.0
         * @return {Proxy}
         */
        getSubject() {
            return this[internalSymbol].subject.getSubject();
        }
    
        /**
         * This method can be used to register commands that can be called via call: instruction.
         * This can be used to provide a pipe with its own functionality.
         *
         * @param {string} name
         * @param {function} callback
         * @returns {Transformer}
         * @throws {TypeError} value is not a string
         * @throws {TypeError} value is not a function
         */
        setCallback(name, callback) {
            this[internalSymbol].callbacks.set(name, callback);
            return this;
        }
    }
    
    /**
     * @private
     * @license AGPLv3
     * @since 1.9.0
     * @return {function
     * @this Updater
     */
    function getCheckStateCallback() {
        return function (current) {
            // this is a reference to the current object (therefore no array function here)
            if (this instanceof HTMLInputElement) {
                if (["radio", "checkbox"].indexOf(this.type) !== -1) {
                    return `${this.value}` === `${current}` ? "true" : undefined;
                }
            } else if (this instanceof HTMLOptionElement) {
                if (isArray(current) && current.indexOf(this.value) !== -1) {
                    return "true";
                }
    
                return undefined;
            }
        };
    }
    
    /**
     * @private
     */
    const symbol = Symbol("@schukai/monster/updater@@EventHandler");
    
    /**
     * @private
     * @return {function}
     * @this Updater
     * @throws {Error} the bind argument must start as a value with a path
     */
    function getControlEventHandler() {
        if (this[symbol]) {
            return this[symbol];
        }
    
        /**
         * @throws {Error} the bind argument must start as a value with a path.
         * @throws {Error} unsupported object
         * @param {Event} event
         */
        this[symbol] = (event) => {
            const element = findTargetElementFromEvent(event, ATTRIBUTE_UPDATER_BIND);
    
            if (element === undefined) {
                return;
            }
            setTimeout(() => {
                retrieveAndSetValue.call(this, element);
            }, 0);
        };
    
        return this[symbol];
    }
    
    /**
     * @throws {Error} the bind argument must start as a value with a path
     * @param {HTMLElement} element
     * @return void
     * @memberOf Monster.DOM
     * @private
     */
    function retrieveAndSetValue(element) {
        const pathfinder = new Pathfinder(this[internalSymbol].subject.getSubject());
    
        let path = element.getAttribute(ATTRIBUTE_UPDATER_BIND);
        if (path === null)
            throw new Error("the bind argument must start as a value with a path");
    
        if (path.indexOf("path:") !== 0) {
            throw new Error("the bind argument must start as a value with a path");
        }
    
        path = path.substring(5); // remove path: from the string
    
        let value;
    
        if (element instanceof HTMLInputElement) {
            switch (element.type) {
                case "checkbox":
                    value = element.checked ? element.value : undefined;
                    break;
                default:
                    value = element.value;
                    break;
            }
        } else if (element instanceof HTMLTextAreaElement) {
            value = element.value;
        } else if (element instanceof HTMLSelectElement) {
            switch (element.type) {
                case "select-one":
                    value = element.value;
                    break;
                case "select-multiple":
                    value = element.value;
    
                    let options = element?.selectedOptions;
                    if (options === undefined)
                        options = element.querySelectorAll(":scope option:checked");
                    value = Array.from(options).map(({value}) => value);
    
                    break;
            }
    
            // values from custom elements
        } else if (
            (element?.constructor?.prototype &&
                !!Object.getOwnPropertyDescriptor(
                    element.constructor.prototype,
                    "value",
                )?.["get"]) ||
            element.hasOwnProperty("value")
        ) {
            value = element?.["value"];
        } else {
            throw new Error("unsupported object");
        }
    
        if (isString(value)) {
            const type = element.getAttribute(ATTRIBUTE_UPDATER_BIND_TYPE);
            switch (type) {
                case "number":
                case "int":
                case "float":
                case "integer":
                    value = Number(value);
                    if (isNaN(value)) {
                        value = 0;
                    }
                    break;
                case "boolean":
                case "bool":
                case "checkbox":
                    value = value === "true" || value === "1" || value === "on";
                    break;
                case "array":
                case "list":
                    value = value.split(",");
                    break;
                case "object":
                case "json":
                    value = JSON.parse(value);
                    break;
                default:
                    break;
            }
        }
    
        const copy = clone(this[internalSymbol].subject.getRealSubject());
    
        const pf = new Pathfinder(copy);
        pf.setVia(path, value);
    
        const diffResult = diff(copy, this[internalSymbol].subject.getRealSubject());
    
        if (diffResult.length > 0) {
            pathfinder.setVia(path, value);
        }
    }
    
    /**
     * @license AGPLv3
     * @since 1.27.0
     * @return void
     * @private
     */
    function retrieveFromBindings() {
        if (this[internalSymbol].element.matches(`[${ATTRIBUTE_UPDATER_BIND}]`)) {
            retrieveAndSetValue.call(this, this[internalSymbol].element);
        }
    
        for (const [, element] of this[internalSymbol].element
            .querySelectorAll(`[${ATTRIBUTE_UPDATER_BIND}]`)
            .entries()) {
            retrieveAndSetValue.call(this, element);
        }
    }
    
    /**
     * @private
     * @license AGPLv3
     * @since 1.8.0
     * @param {object} change
     * @return {void}
     */
    function removeElement(change) {
        for (const [, element] of this[internalSymbol].element
            .querySelectorAll(`:scope [${ATTRIBUTE_UPDATER_REMOVE}]`)
            .entries()) {
            element.parentNode.removeChild(element);
        }
    }
    
    /**
     * @private
     * @license AGPLv3
     * @since 1.8.0
     * @param {object} change
     * @return {void}
     * @throws {Error} the value is not iterable
     * @throws {Error} pipes are not allowed when cloning a node.
     * @throws {Error} no template was found with the specified key.
     * @throws {Error} the maximum depth for the recursion is reached.
     * @this Updater
     */
    function insertElement(change) {
        const subject = this[internalSymbol].subject.getRealSubject();
    
        const mem = new WeakSet();
        let wd = 0;
    
        const container = this[internalSymbol].element;
    
        while (true) {
            let found = false;
            wd++;
    
            const p = clone(change?.["path"]);
            if (!isArray(p)) return;
    
            while (p.length > 0) {
                const current = p.join(".");
    
                let iterator = new Set();
                const query = `[${ATTRIBUTE_UPDATER_INSERT}*="path:${current}"]`;
    
                const e = container.querySelectorAll(query);
    
                if (e.length > 0) {
                    iterator = new Set([...e]);
                }
    
                if (container.matches(query)) {
                    iterator.add(container);
                }
    
                for (const [, containerElement] of iterator.entries()) {
                    if (mem.has(containerElement)) continue;
                    mem.add(containerElement);
    
                    found = true;
    
                    const attributes = containerElement.getAttribute(
                        ATTRIBUTE_UPDATER_INSERT,
                    );
                    if (attributes === null) continue;
    
                    const def = trimSpaces(attributes);
                    const i = def.indexOf(" ");
                    const key = trimSpaces(def.substr(0, i));
                    const refPrefix = `${key}-`;
                    const cmd = trimSpaces(def.substr(i));
    
                    // this case is actually excluded by the query but is nevertheless checked again here
                    if (cmd.indexOf("|") > 0) {
                        throw new Error("pipes are not allowed when cloning a node.");
                    }
    
                    const pipe = new Pipe(cmd);
                    this[internalSymbol].callbacks.forEach((f, n) => {
                        pipe.setCallback(n, f);
                    });
    
                    let value;
                    try {
                        containerElement.removeAttribute(ATTRIBUTE_ERRORMESSAGE);
                        value = pipe.run(subject);
                    } catch (e) {
                        containerElement.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.message);
                    }
    
                    const dataPath = cmd.split(":").pop();
    
                    let insertPoint;
                    if (containerElement.hasChildNodes()) {
                        insertPoint = containerElement.lastChild;
                    }
    
                    if (!isIterable(value)) {
                        throw new Error("the value is not iterable");
                    }
    
                    const available = new Set();
    
                    for (const [i] of Object.entries(value)) {
                        const ref = refPrefix + i;
                        const currentPath = `${dataPath}.${i}`;
    
                        available.add(ref);
                        const refElement = containerElement.querySelector(
                            `[${ATTRIBUTE_UPDATER_INSERT_REFERENCE}="${ref}"]`,
                        );
    
                        if (refElement instanceof HTMLElement) {
                            insertPoint = refElement;
                            continue;
                        }
    
                        appendNewDocumentFragment(containerElement, key, ref, currentPath);
                    }
    
                    const nodes = containerElement.querySelectorAll(
                        `[${ATTRIBUTE_UPDATER_INSERT_REFERENCE}*="${refPrefix}"]`,
                    );
    
                    for (const [, node] of Object.entries(nodes)) {
                        if (
                            !available.has(
                                node.getAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE),
                            )
                        ) {
                            try {
                                containerElement.removeChild(node);
                            } catch (e) {
                                containerElement.setAttribute(
                                    ATTRIBUTE_ERRORMESSAGE,
                                    `${containerElement.getAttribute(ATTRIBUTE_ERRORMESSAGE)}, ${
                                        e.message
                                    }`.trim(),
                                );
                            }
                        }
                    }
                }
    
                p.pop();
            }
    
            if (found === false) break;
            if (wd++ > 200) {
                throw new Error("the maximum depth for the recursion is reached.");
            }
        }
    }
    
    /**
     *
     * @private
     * @license AGPLv3
     * @since 1.8.0
     * @param {HTMLElement} container
     * @param {string} key
     * @param {string} ref
     * @param {string} path
     * @throws {Error} no template was found with the specified key.
     */
    function appendNewDocumentFragment(container, key, ref, path) {
        const template = findDocumentTemplate(key, container);
    
        const nodes = template.createDocumentFragment();
        for (const [, node] of Object.entries(nodes.childNodes)) {
            if (node instanceof HTMLElement) {
                applyRecursive(node, key, path);
                node.setAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE, ref);
            }
    
            container.appendChild(node);
        }
    }
    
    /**
     * @private
     * @license AGPLv3
     * @since 1.10.0
     * @param {HTMLElement} node
     * @param {string} key
     * @param {string} path
     * @return {void}
     */
    function applyRecursive(node, key, path) {
        if (node instanceof HTMLElement) {
            if (node.hasAttribute(ATTRIBUTE_UPDATER_REPLACE)) {
                const value = node.getAttribute(ATTRIBUTE_UPDATER_REPLACE);
                node.setAttribute(
                    ATTRIBUTE_UPDATER_REPLACE,
                    value.replaceAll(`path:${key}`, `path:${path}`),
                );
            }
    
            if (node.hasAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES)) {
                const value = node.getAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES);
                node.setAttribute(
                    ATTRIBUTE_UPDATER_ATTRIBUTES,
                    value.replaceAll(`path:${key}`, `path:${path}`),
                );
            }
    
            for (const [, child] of Object.entries(node.childNodes)) {
                applyRecursive(child, key, path);
            }
        }
    }
    
    /**
     * @private
     * @license AGPLv3
     * @since 1.8.0
     * @param {object} change
     * @return {void}
     * @this Updater
     */
    function updateContent(change) {
        const subject = this[internalSymbol].subject.getRealSubject();
    
        const p = clone(change?.["path"]);
        runUpdateContent.call(this, this[internalSymbol].element, p, subject);
    
        const slots = this[internalSymbol].element.querySelectorAll("slot");
        if (slots.length > 0) {
            for (const [, slot] of Object.entries(slots)) {
                for (const [, element] of Object.entries(slot.assignedNodes())) {
                    runUpdateContent.call(this, element, p, subject);
                }
            }
        }
    }
    
    /**
     * @private
     * @license AGPLv3
     * @since 1.8.0
     * @param {HTMLElement} container
     * @param {array} parts
     * @param {object} subject
     * @return {void}
     */
    function runUpdateContent(container, parts, subject) {
        if (!isArray(parts)) return;
        if (!(container instanceof HTMLElement)) return;
        parts = clone(parts);
    
        const mem = new WeakSet();
    
        while (parts.length > 0) {
            const current = parts.join(".");
            parts.pop();
    
            // Unfortunately, static data is always changed as well, since it is not possible to react to changes here.
            const query = `[${ATTRIBUTE_UPDATER_REPLACE}^="path:${current}"], [${ATTRIBUTE_UPDATER_REPLACE}^="static:"], [${ATTRIBUTE_UPDATER_REPLACE}^="i18n:"]`;
            const e = container.querySelectorAll(`${query}`);
    
            const iterator = new Set([...e]);
    
            if (container.matches(query)) {
                iterator.add(container);
            }
    
            /**
             * @type {HTMLElement}
             */
            for (const [element] of iterator.entries()) {
                if (mem.has(element)) return;
                mem.add(element);
    
                const attributes = element.getAttribute(ATTRIBUTE_UPDATER_REPLACE);
                const cmd = trimSpaces(attributes);
    
                const pipe = new Pipe(cmd);
                this[internalSymbol].callbacks.forEach((f, n) => {
                    pipe.setCallback(n, f);
                });
    
                let value;
                try {
                    element.removeAttribute(ATTRIBUTE_ERRORMESSAGE);
                    value = pipe.run(subject);
                } catch (e) {
                    element.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.message);
                }
    
                if (value instanceof HTMLElement) {
                    while (element.firstChild) {
                        element.removeChild(element.firstChild);
                    }
    
                    try {
                        element.appendChild(value);
                    } catch (e) {
                        element.setAttribute(
                            ATTRIBUTE_ERRORMESSAGE,
                            `${element.getAttribute(ATTRIBUTE_ERRORMESSAGE)}, ${
                                e.message
                            }`.trim(),
                        );
                    }
                } else {
                    element.innerHTML = value;
                }
            }
        }
    }
    
    /**
     * @private
     * @since 1.8.0
     * @param {object} change
     * @return {void}
     */
    function updateAttributes(change) {
        const subject = this[internalSymbol].subject.getRealSubject();
        const p = clone(change?.["path"]);
        runUpdateAttributes.call(this, this[internalSymbol].element, p, subject);
    }
    
    /**
     * @private
     * @param {HTMLElement} container
     * @param {array} parts
     * @param {object} subject
     * @return {void}
     * @this Updater
     */
    function runUpdateAttributes(container, parts, subject) {
        if (!isArray(parts)) return;
        parts = clone(parts);
    
        const mem = new WeakSet();
    
        while (parts.length > 0) {
            const current = parts.join(".");
            parts.pop();
    
            let iterator = new Set();
    
            const query = `[${ATTRIBUTE_UPDATER_SELECT_THIS}][${ATTRIBUTE_UPDATER_ATTRIBUTES}], [${ATTRIBUTE_UPDATER_ATTRIBUTES}*="path:${current}"], [${ATTRIBUTE_UPDATER_ATTRIBUTES}^="static:"], [${ATTRIBUTE_UPDATER_ATTRIBUTES}^="i18n:"]`;
    
            const e = container.querySelectorAll(query);
    
            if (e.length > 0) {
                iterator = new Set([...e]);
            }
    
            if (container.matches(query)) {
                iterator.add(container);
            }
    
            for (const [element] of iterator.entries()) {
                if (mem.has(element)) return;
                mem.add(element);
    
                // this case occurs when the ATTRIBUTE_UPDATER_SELECT_THIS attribute is set
                if (!element.hasAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES)) {
                    continue;
                }
    
                const attributes = element.getAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES);
    
                for (let [, def] of Object.entries(attributes.split(","))) {
                    def = trimSpaces(def);
                    const i = def.indexOf(" ");
                    const name = trimSpaces(def.substr(0, i));
                    const cmd = trimSpaces(def.substr(i));
    
                    const pipe = new Pipe(cmd);
    
                    this[internalSymbol].callbacks.forEach((f, n) => {
                        pipe.setCallback(n, f, element);
                    });
    
                    let value;
                    try {
                        element.removeAttribute(ATTRIBUTE_ERRORMESSAGE);
                        value = pipe.run(subject);
                    } catch (e) {
                        element.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.message);
                    }
    
                    if (value === undefined) {
                        element.removeAttribute(name);
                    } else if (element.getAttribute(name) !== value) {
                        element.setAttribute(name, value);
                    }
    
                    handleInputControlAttributeUpdate.call(this, element, name, value);
                }
            }
        }
    }
    
    /**
     * @private
     * @param {HTMLElement|*} element
     * @param {string} name
     * @param {string|number|undefined} value
     * @return {void}
     * @this Updater
     */
    
    function handleInputControlAttributeUpdate(element, name, value) {
        if (element instanceof HTMLSelectElement) {
            switch (element.type) {
                case "select-multiple":
                    for (const [index, opt] of Object.entries(element.options)) {
                        if (value.indexOf(opt.value) !== -1) {
                            opt.selected = true;
                        } else {
                            opt.selected = false;
                        }
                    }
    
                    break;
                case "select-one":
                    // Only one value may be selected
    
                    for (const [index, opt] of Object.entries(element.options)) {
                        if (opt.value === value) {
                            element.selectedIndex = index;
                            break;
                        }
                    }
    
                    break;
            }
        } else if (element instanceof HTMLInputElement) {
            switch (element.type) {
                case "radio":
                    if (name === "checked") {
                        element.checked = value !== undefined;
                    }
    
                    break;
    
                case "checkbox":
                    if (name === "checked") {
                        element.checked = value !== undefined;
                    }
    
                    break;
                case "text":
                default:
                    if (name === "value") {
                        element.value = value === undefined ? "" : value;
                    }
    
                    break;
            }
        } else if (element instanceof HTMLTextAreaElement) {
            if (name === "value") {
                element.value = value === undefined ? "" : value;
            }
        }
    }
    
    /**
     * @param {NodeList|HTMLElement|Set<HTMLElement>} elements
     * @param {Symbol} symbol
     * @param {object} object
     * @param {object} config
     *
     * Config: enableEventProcessing {boolean} - default: false - enables the event processing
     *
     * @return {Promise[]}
     * @license AGPLv3
     * @since 1.23.0
     * @memberOf Monster.DOM
     * @throws {TypeError} elements is not an instance of NodeList, HTMLElement or Set
     * @throws {TypeError} the context of the function is not an instance of HTMLElement
     * @throws {TypeError} symbol must be an instance of Symbol
     */
    function addObjectWithUpdaterToElement(elements, symbol, object, config = {}) {
        if (!(this instanceof HTMLElement)) {
            throw new TypeError(
                "the context of this function must be an instance of HTMLElement",
            );
        }
    
        if (!(typeof symbol === "symbol")) {
            throw new TypeError("symbol must be an instance of Symbol");
        }
    
        const updaters = new Set();
    
        if (elements instanceof NodeList) {
            elements = new Set([...elements]);
        } else if (elements instanceof HTMLElement) {
            elements = new Set([elements]);
        } else if (elements instanceof Set) {
        } else {
            throw new TypeError(
                `elements is not a valid type. (actual: ${typeof elements})`,
            );
        }
    
        const result = [];
    
        const updaterCallbacks = [];
        const cb = this?.[updaterTransformerMethodsSymbol];
        if (this instanceof HTMLElement && typeof cb === "function") {
            const callbacks = cb.call(this);
            if (typeof callbacks === "object") {
                for (const [name, callback] of Object.entries(callbacks)) {
                    if (typeof callback === "function") {
                        updaterCallbacks.push([name, callback]);
                    } else {
                        addAttributeToken(
                            this,
                            ATTRIBUTE_ERRORMESSAGE,
                            `onUpdaterPipeCallbacks: ${name} is not a function`,
                        );
                    }
                }
            } else {
                addAttributeToken(
                    this,
                    ATTRIBUTE_ERRORMESSAGE,
                    `onUpdaterPipeCallbacks do not return an object with functions`,
                );
            }
        }
    
        elements.forEach((element) => {
            if (!(element instanceof HTMLElement)) return;
            if (element instanceof HTMLTemplateElement) return;
    
            const u = new Updater(element, object);
            updaters.add(u);
    
            if (updaterCallbacks.length > 0) {
                for (const [name, callback] of updaterCallbacks) {
                    u.setCallback(name, callback);
                }
            }
    
            result.push(
                u.run().then(() => {
                    if (config.eventProcessing === true) {
                        u.enableEventProcessing();
                    }
    
                    return u;
                }),
            );
        });
    
        if (updaters.size > 0) {
            addToObjectLink(this, symbol, updaters);
        }
    
        return result;
    }