UNPKG

@seanox/aspect-js

Version:

full stack JavaScript framework for SPAs incl. reactivity rendering, mvc / mvvm, models, expression language, datasource, routing, paths, unit test and some more

1,007 lines (893 loc) 153 kB
/** * LIZENZBEDINGUNGEN - Seanox Software Solutions ist ein Open-Source-Projekt, * im Folgenden Seanox Software Solutions oder kurz Seanox genannt. * Diese Software unterliegt der Version 2 der Apache License. * * Seanox aspect-js, fullstack for single page applications * Copyright (C) 2025 Seanox Software Solutions * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * * * DESCRIPTION * ---- * * With aspect-js the declarative approach of HTML is taken up and extended. * In addition to the expression language, the HTML elements are provided with * additional attributes for functions and view model binding. The corresponding * renderer is included in the composite implementation and actively monitors * the DOM via the MutationObserver and thus reacts recursively to changes in * the DOM. * * This is the static component for rendering and the view model binding. * Processing runs in the background and starts automatically when the page is * loaded. * * * TERMS * ---- * * namespace * ---- * Comparable to packages in other programming languages, namespaces can be used * for hierarchical structuring of components, resources and business logic. * Although packages are not a feature of JavaScript, they can be mapped at the * object level by concatenating objects into an object tree. Here, each level * of the object tree forms a namespace, which can also be considered a domain. * * As is typical for the identifiers of objects, namespaces also use letters, * numbers and underscores separated by a dot. As a special feature, arrays are * also supported. If a layer in the namespace uses an integer, this layer is * used as an array. * * model * ---- * Models are representable/projectable static JavaScript objects that can * provide and receive data, states and interactions for views, comparable to * managed beans and DTOs (Data Transfer Objects). As singleton/facade/delegate, * they can use other components and abstractions, contain business logic * themselves, and be a link between the user interface (view) and middleware * (backend). * * The required view model binding is part of the Model View Controller and the * Composite API. * * property * ---- * Is a property in a static model (model component / component). It * corresponds to an HTML element with the same ID in the same namespace. The * ID of the property can be relative or use an absolute namespace. If the ID * is relative, the namespace is defined by the parent composite element. * * qualifier * ---- * In some cases, the identifier (ID) may not be unique. For example, in cases * where properties are arrays or an iteration is used. In these cases the * identifier can be extended by an additional unique qualifier separated by a * colon. Qualifiers behave like properties during view model binding and extend * the namespace. * * unique * ---- * In some cases it is necessary that different things in the view refer to a * target in the corresponding model. However, if these things must still be * unique in the view, a unique identifier can be used in addition to the * qualifier, separated by a hash. This identifier then has no influence on the * composite logic and is used exclusively for the view. * * composite * ---- * Composite describes a construct of markup (view), JavaScript object (model), * CSS and possibly other resources. It describes a component/module without * direct relation to the representation. * * composite-id * ---- * It is a character sequence consisting of letters, numbers and underscores * and optionally supports the minus sign if it is not used at the beginning or * end. A composite ID is at least one character long and is composed by * combining the attributes ID and COMPOSITE. * * * PRINCIPLES * ---- * * The world is static. So also aspect-js and all components. This avoids the * management and establishment of instances. * * Attributes ID and COMPOSITE are elementary and immutable. * They are read the first time an element occurs and stored in the object * cache. Therefore, these attributes cannot be changed later because they are * then used from the object cache. * * Attributes of elements are elementary and immutable even if they contain an * expression. * * Clean Code Rendering - The aspect-js relevant attributes are stored in * meta-objects to each element and are removed in the markup. The following * attributes are essential: COMPOSITE, ID -- they are cached and remain at the * markup, these cannot be changed. the MutationObserver will restore them. */ (() => { "use strict"; // Storage for variables with the page scope const _render_context_scope = []; // Storage for dynamic/temporary variables with the page scope const _render_context_stack = []; // Storage for the currently used dynamic/temporary variables with the page // scope. The storages _render_context_scope and _render_context_stack are // used to manage the variables. So that these remain clean and the elements // can use their initial context when rendering without manipulating // _render_context_stack. This applies in particular to the generated // children with their own meta-objects when iterating. const _render_context_workspace = []; compliant("Composite"); compliant(null, window.Composite = { // Against the trend, the constants of the composite are public so that // they can be used by extensions. /** Path of the Composite for: modules (sub-directory of work path) */ get MODULES() {return window.location.combine(window.location.contextPath, "/modules");}, /** Constant for attribute composite */ get ATTRIBUTE_COMPOSITE() {return "composite";}, /** Constant for attribute condition */ get ATTRIBUTE_CONDITION() {return "condition";}, /** Constant for attribute events */ get ATTRIBUTE_EVENTS() {return "events";}, /** Constant for attribute id */ get ATTRIBUTE_ID() {return "id";}, /** Constant for attribute import */ get ATTRIBUTE_IMPORT() {return "import";}, /** Constant for attribute interval */ get ATTRIBUTE_INTERVAL() {return "interval";}, /** Constant for attribute iterate */ get ATTRIBUTE_ITERATE() {return "iterate";}, /** Constant for attribute message */ get ATTRIBUTE_MESSAGE() {return "message";}, /** Constant for attribute name */ get ATTRIBUTE_NAME() {return "name";}, /** Constant for attribute output */ get ATTRIBUTE_OUTPUT() {return "output";}, /** Constant for attribute render */ get ATTRIBUTE_RENDER() {return "render";}, /** Constant for attribute release */ get ATTRIBUTE_RELEASE() {return "release";}, /** Constant for attribute text */ get ATTRIBUTE_TEXT() {return "text";}, /** Constant for attribute type */ get ATTRIBUTE_TYPE() {return "type";}, /** Constant for attribute validate */ get ATTRIBUTE_VALIDATE() {return "validate";}, /** Constant for attribute value */ get ATTRIBUTE_VALUE() {return "value";}, /** * Pattern for all accepted attributes. * Accepted attributes are all attributes, even without an expression * that is cached in the meta-object. Other attributes are only cached * if they contain an expression. */ get PATTERN_ATTRIBUTE_ACCEPT() {return /^(composite|condition|events|id|import|interval|iterate|message|output|release|render|validate)$/i;}, /** * Pattern for all static attributes. * Static attributes are not removed from the element during rendering, * but are also set in the meta-object like non-static attributes. These * attributes are also intended for direct use in JavaScript and CSS. */ get PATTERN_ATTRIBUTE_STATIC() {return /^(composite|id)$/i;}, /** * Pattern to detect if a string contains an expression. * Escaping characters via slash is supported. */ get PATTERN_EXPRESSION_CONTAINS() {return /\{\{(.|\r|\n)*?\}\}/g;}, /** * Patterns for condition expressions. * Conditions are explicitly a single expression and not a variable * expression. */ get PATTERN_EXPRESSION_CONDITION() {return /^\s*\{\{\s*(([^}]|(}(?!})))*?)\s*\}\}\s*$/i;}, /** * Patterns for expressions with variable. * Variables are at the beginning of the expression and are separated * from the expression by a colon. The variable name must conform to the * usual JavaScript conditions and starts with _ or a letter, other word * characters (_ 0-9 a-z A-Z) may follow. * - group 1: variable * - group 2: expression */ get PATTERN_EXPRESSION_VARIABLE() {return /^\s*\{\{\s*((?:(?:_*[a-z])|(?:_\w*))\w*)\s*:\s*(([^}]|(}(?!})))*?)\s*\}\}\s*$/i;}, /** Pattern for all to ignore (script-)elements */ get PATTERN_ELEMENT_IGNORE() {return /script|style/i;}, /** Pattern for all script elements */ get PATTERN_SCRIPT() {return /script/i;}, /** * Pattern for all composite-script elements. * These elements are not automatically executed by the browser but must * be triggered by rendering. Therefore, these scripts can be combined * and controlled with ATTRIBUTE_CONDITION. */ get PATTERN_COMPOSITE_SCRIPT() {return /^composite\/javascript$/i;}, /** * Pattern for a composite id (based on a word e.g. name@namespace:...) * - group 1: name * - group 2: namespace (optional) */ get PATTERN_COMPOSITE_ID() {return /^([_a-z]\w*)(?:@([_a-z]\w*(?::[_a-z]\w*)*))?$/i;}, /** * Pattern for an element id (e.g. name:qualifier...@model:...) * - group 1: name * - group 2: qualifier(s) (optional) * - group 3: unique identifier (optional) * - group 4: (namespace+)model (optional) */ get PATTERN_ELEMENT_ID() {return /^([_a-z]\w*)(?::(\w+(?::\w+)*))?(?:#(\w+))?(?:@([_a-z]\w*(?::[_a-z]\w*)*))?$/i;}, /** Pattern for a scope (custom tag, based on a word) */ get PATTERN_CUSTOMIZE_SCOPE() {return /[_a-z]([\w-]*\w)?$/i;}, /** Pattern for a datasource url */ get PATTERN_DATASOURCE_URL() {return /^\s*xml:\s*(\/\S+)\s*(?:\s*(?:xslt|xsl):\s*(\/\S+))*$/i;}, /** Pattern for all accepted events */ get PATTERN_EVENT() {return /^([A-Z][a-z]+)+$/;}, /** Constants of events during rendering */ get EVENT_RENDER_START() {return "RenderStart";}, get EVENT_RENDER_NEXT() {return "RenderNext";}, get EVENT_RENDER_END() {return "RenderEnd";}, /** Constants of events during mounting */ get EVENT_MOUNT_START() {return "MountStart";}, get EVENT_MOUNT_NEXT() {return "MountNext";}, get EVENT_MOUNT_END() {return "MountEnd";}, /** Constants of events when using modules */ get EVENT_MODULE_LOAD() {return "ModuleLoad";}, get EVENT_MODULE_DOCK() {return "ModuleDock";}, get EVENT_MODULE_READY() {return "ModuleReady";}, get EVENT_MODULE_UNDOCK() {return "ModuleUndock";}, /** Constants of events when using HTTP */ get EVENT_HTTP_START() {return "HttpStart";}, get EVENT_HTTP_PROGRESS() {return "HttpProgress";}, get EVENT_HTTP_RECEIVE() {return "HttpReceive";}, get EVENT_HTTP_LOAD() {return "HttpLoad";}, get EVENT_HTTP_ABORT() {return "HttpAbort";}, get EVENT_HTTP_TIMEOUT() {return "HttpTimeout";}, get EVENT_HTTP_ERROR() {return "HttpError";}, get EVENT_HTTP_END() {return "HttpEnd";}, /** Constants of events when errors occur */ get EVENT_ERROR() {return "Error";}, /** * List of possible DOM events * see also https://www.w3schools.com/jsref/dom_obj_event.asp */ get EVENTS() {return "abort after|print animation|end animation|iteration animation|start" + " before|print before|unload blur" + " can|play can|play|through change click context|menu copy cut" + " dbl|click drag drag|end drag|enter drag|leave drag|over drag|start drop duration|change" + " ended error" + " focus focus|in focus|out" + " hash|change" + " input invalid" + " key|down key|press key|up" + " load loaded|data loaded|meta|data load|start" + " message mouse|down mouse|enter mouse|leave mouse|move mouse|over mouse|out mouse|up mouse|wheel" + " offline online open" + " page|hide page|show paste pause play playing popstate progress" + " rate|change resize reset" + " scroll search seeked seeking select show stalled storage submit suspend" + " time|update toggle touch|cancel touch|end touch|move touch|start transition|end" + " unload" + " volume|change" + " waiting wheel";}, /** Patterns with the supported events */ get PATTERN_EVENT_FUNCTIONS() {return (() => { const pattern = Composite.EVENTS.replace(/(?:\||\b)(\w)/g, (match, letter) => letter.toUpperCase()); return new RegExp("^on(" + pattern.replace(/\s+/g, "|") + ")"); })();}, /** Patterns with the supported events as plain array */ get PATTERN_EVENT_NAMES() {return (() => { return Composite.EVENTS.replace(/(?:\||\b)(\w)/g, (match, letter) => letter.toUpperCase()).split(/\s+/); })();}, /** Patterns with the supported events as plain array (lower case) */ get PATTERN_EVENT_FILTER() {return (() => { return Composite.EVENTS.replace(/(?:\||\b)(\w)/g, (match, letter) => letter.toUpperCase()).toLowerCase().split(/\s+/); })();}, /** * Lock mechanism for methods: render, mound and scan. The lock controls * that the methods are not used concurrently and/or asynchronously. * Each method opens its own transaction (lock). During a transaction, * the method call requires a lock. If this lock does not exist or * differs from the current transaction, the method call is parked in a * queue until the current lock is released. The methods themselves can * call themselves recursively and do so with the lock they know. In * addition to the lock mechanism, the methods also control the START, * NEXT, and END events. * @param {Object} context Context (render, mound or scan) * @param {string|Element} selector Selector to identify elements * @returns {Object} The created lock as a meta-object * @throws {Error} In case of an invalid context */ lock(context, selector) { context.queue = context.queue || []; if (context.lock === undefined || context.lock === false) { context.lock = {ticks:1, selector, queue:[], share() { this.ticks++; return this; }, release() { this.ticks--; if (this.ticks > 0) return; if (context === Composite.render) { // To ensure that on conditions when the lock is // created for the marker, the children are also // mounted, the selector must be switched to the // element, because the marker is a text node // without children. let selector = this.selector; if (selector instanceof Node && selector.nodeType === Node.TEXT_NODE) { let serial = selector.ordinal(); let object = _render_meta[serial] || {}; if (object.condition && object.condition.element && object.condition.marker === this.selector) selector = object.condition.element; } // If the selector is a string, several elements // must be assumed, which may or may not have a // relation to the DOM. Therefore, they are all // considered and mounted separately. let nodes = []; if (typeof selector === "string") { const scope = document.querySelectorAll(selector); Array.from(scope).forEach((node) => { if (!nodes.includes(node)) nodes.push(node); const scope = node.querySelectorAll("*"); Array.from(scope).forEach((node) => { if (!nodes.includes(node)) nodes.push(node); }); }); } else if (selector instanceof Element) { nodes = selector.querySelectorAll("*"); nodes = [selector].concat(Array.from(nodes)); } // Mount all elements in a composite, including // the composite element itself nodes.forEach((node) => Composite.mount(node)); Composite.fire(Composite.EVENT_RENDER_END, this.selector); } else if (context === Composite.mount) { Composite.fire(Composite.EVENT_MOUNT_END, this.selector); } else throw new Error("Invalid context: " + context); const selector = context.queue.shift(); if (selector) Composite.asynchron(context, selector); context.lock = false; }}; if (context === Composite.render) Composite.fire(Composite.EVENT_RENDER_START, selector); else if (context === Composite.mount) Composite.fire(Composite.EVENT_MOUNT_START, selector); else throw new Error("Invalid context: " + context); } else { if (context === Composite.render) Composite.fire(Composite.EVENT_RENDER_NEXT, selector); else if (context === Composite.mount) Composite.fire(Composite.EVENT_MOUNT_NEXT, selector); else throw new Error("Invalid context: " + context); } return context.lock; }, /** * Registers a callback function for composite events. * @param {string} event Event type (see Composite.EVENT_***) * @param {function} callback Callback function to be registered * @throws {TypeError} In case of invalid event type * @throws {TypeError} In case of invalid callback type * @throws {Error} In the following cases: * - Event is not valid or is not supported * - Callback is not implemented correctly or does not exist */ listen(event, callback) { if (typeof event !== "string") throw new TypeError("Invalid event: " + typeof event); if (typeof callback !== "function" && callback !== null && callback !== undefined) throw new TypeError("Invalid callback: " + typeof callback); if (!event.match(Composite.PATTERN_EVENT)) throw new Error(`Invalid event${event.trim() ? ": " + event : ""}`); event = event.toLowerCase(); if (!_listeners.has(event) || !Array.isArray(_listeners.get(event))) _listeners.set(event, []); _listeners.get(event).push(callback); }, /** * Triggers an event. * All callback functions for this event are called. * @param {string} event Event type (see Composite.EVENT_***) * @param {...*} variants Up to five additional optional arguments * passed to the callback function */ fire(event, ...variants) { event = (event || "").trim(); if (_listeners.size <= 0 || !event) return; const listeners = _listeners.get(event.toLowerCase()); if (!Array.isArray(listeners)) return; variants = [event, ...variants]; listeners.forEach((callback) => callback(...variants)); }, /** * Asynchronous or in reality non-blocking call of a function. Because * the asynchronous execution is not possible without Web Worker. * @param {function} task Function to be executed * @param {...*} variants Up to five additional optional arguments * passed to the callback function */ asynchron(task, ...variants) { window.setTimeout((invoke, ...variants) => { invoke(...variants); }, 0, task, ...variants); }, /** * Determines the corresponding meta-object for the passed selector. * @param {Element|string} selector, as DOM element or a string * @returns {object|null} the determined meta-object or null * @throws {Error} If the selector is not unique (several nodes found). */ lookup(selector) { if (selector == null) return null; if (typeof selector === "string") { const nodes = document.querySelectorAll(selector.trim()); if (nodes.length <= 0) return null; if (nodes.length > 1) throw new Error("Selector is not unique"); return Composite.lookup(nodes[0]); } if (!(selector instanceof Element)) return null; const lookup = _mount_lookup(selector); if (lookup !== null) delete lookup.target; return lookup; }, /** * Validates the as selector passed element(s), if the element(s) are * marked with ATTRIBUTE_VALIDATE, a two-step validation is performed. * In the first step, the HTML5 validation is checked if it exists. If * this validation is valid or does not exist, the model based * validation is executed if it exists. For this purpose, the static * method validate is expected in the model. The current element and the * current value (if available) are passed as arguments. * * The validation can have four states: * * true, not true, text, undefined/void * * true * ---- * The validation was successful. No error is displayed and the default * action of the browser is used. If possible the value is synchronized * with the model. * * not true and not undefined/void * ---- * The validation failed; an error is displayed. An existing return * value indicates that the default action of the browser should not be * executed and so it is blocked. In this case, a possible value is not * synchronized with the model. * * text * ---- * Text corresponds to: Invalid + error message. If the error message is * empty, the message from ATTRIBUTE_MESSAGE is used alternatively. * * undefined/void * ---- * The validation failed; an error is displayed. No return value * indicates that the default action of the browser should nevertheless * be executed. This behavior is important e.g. for the validation of * input fields, so that the input reaches the user interface. In this * case, a possible value is not synchronized with the model. * * @param {Element|string} selector DOM element or a string * @param {boolean} [lock=true] Unlocking of the model validation * @returns {boolean|undefined} Validation result * true, false, undefined/void * @throws {Error} In case of a non-unique selector */ validate(selector, lock) { if (arguments.length < 2 || lock !== false) lock = true; if (typeof selector === "string") { selector = selector.trim(); if (!selector) return; let validate = Array.from(document.querySelectorAll(selector)); validate.forEach((node, index) => { validate[index] = Composite.validate(node, lock); if (validate[index] === undefined) validate[index] = 0; else validate[index] = validate[index] === true ? 1 : 2; }); validate = validate.join(""); if (validate.match(/^1+$/)) return true; if (validate.match(/2/)) return false; return; } if (!(selector instanceof Element)) return; const serial = selector.ordinal(); const object = _render_meta[serial]; let valid = true; // Resets the customer-specific error. // This is necessary for the checkValidity method to work. if (typeof selector.setCustomValidity === "function") selector.setCustomValidity(""); // Explicit validation via HTML5. If the validation fails, model // validation and synchronization is not and rendering always // performed. In this case the event and thus the default action of // the browser is cancelled. if (typeof selector.checkValidity === "function") valid = selector.checkValidity(); // There can be a corresponding model. const meta = _mount_lookup(selector); if (meta instanceof Object) { // Validation is a function at the model level. If a composite // consists of several model levels, the validation may have to // be organized accordingly if necessary. Interactive composite // elements are a property object. Therefore, they are primarily // a property and the validation is located in the surrounding // model and not in the property object itself. let value; if (selector instanceof Element) { if (selector.tagName.match(/^input$/i) && selector.type.match(/^radio|checkbox/i)) value = selector.checked; else if (selector.tagName.match(/^select/i) && "selectedIndex" in selector) value = selector.options[selector.selectedIndex].value; else if (Composite.ATTRIBUTE_VALUE in selector) value = selector[Composite.ATTRIBUTE_VALUE]; } // Implicit validation via the model, if a corresponding // validate method is implemented. The validation through the // model only works if the corresponding composite is // active/present in the DOM! if (object.attributes.hasOwnProperty(Composite.ATTRIBUTE_VALIDATE) && valid === true && lock !== true && typeof meta.model[Composite.ATTRIBUTE_VALIDATE] === "function") { const validate = meta.model[Composite.ATTRIBUTE_VALIDATE]; if (value !== undefined) valid = validate.call(meta.model, selector, value); else valid = validate.call(meta.model, selector); } } // ATTRIBUTE_VALIDATE can be supplemented with ATTRIBUTE_MESSAGE. // However, ATTRIBUTE_MESSAGE has no effect without // ATTRIBUTE_VALIDATE. The value of ATTRIBUTE_MESSAGE is used as an // error message if the validation was not successful. For this // purpose, the browser function of HTML5 form validation is used, // which shows the message as a browser validation tooltip/message. // // The browser validation tooltip/message can be redirected to an // attribute of the validated element if the message begins with the // prefix '@<attribute>:', which includes the return value of // expressions. Redirection to static and protected attributes is // ignored and the message is suppressed for these destinations. // This redirection can be helpful if a custom error concept needs // to be implemented. if (typeof selector.setCustomValidity === "function") { if (valid === true && Object.usable(object.message) && Object.usable(object.message.attribute)) selector.removeAttribute(object.message.attribute); if (valid !== true) { let message; if (typeof valid === "string" && valid.trim()) message = valid.trim(); if (typeof message !== "string") { if (object.attributes.hasOwnProperty(Composite.ATTRIBUTE_MESSAGE)) message = String(object.attributes[Composite.ATTRIBUTE_MESSAGE] || ""); if ((message || "").match(Composite.PATTERN_EXPRESSION_CONTAINS)) message = String(Expression.eval(serial + ":" + Composite.ATTRIBUTE_MESSAGE, message)); } if (Object.usable(message)) { const PATTERN_MESSAGE_REDIRECT = /^@([_a-z](?:[\w-]*\w)?):\s*(.*?)\s*$/i; const redirect = message.match(PATTERN_MESSAGE_REDIRECT); if (redirect) { const attribute = redirect[1]; if (!(object.statics || {}).hasOwnProperty(attribute.toLowerCase()) && !Composite.PATTERN_ATTRIBUTE_STATIC.test(attribute)) { object.message = {attribute:attribute}; selector.setAttribute(attribute, redirect[2]); } selector.setCustomValidity(redirect[2]); } else { selector.setCustomValidity(message); if (typeof selector.reportValidity === "function") selector.reportValidity(); } } } } if (valid === undefined) return; return valid; }, /** * Mounts the as selector passed element(s) with all its children where * a view model binding is possible. Mount is possible for all elements * with ATTRIBUTE_ID, not only for composite objects and their children. * * View-model binding is about linking of HTML elements in markup (view) * with corresponding JavaScript objects (models). * * Models are representable/projectable static JavaScript objects that * can provide and receive data, states and interactions for views, * comparable to managed beans and DTOs. As singleton/facade/delegate, * they can use other components and abstractions, contain business * logic themselves, and be a link between the user interface (view) and * middleware (backend). * * The required view model binding is part of the Model View Controller * and the Composite API. * * The view as presentation and user interface for interactions and the * model are primarily decoupled. For the MVVM approach as an extension * of the MVC, the controller establishes the bidirectional connection * between view and model, which means that no manual implementation and * declaration of events, interaction or synchronization is required. * * Principles * ---- * Components are a static JavaScript objects (models). Namespaces are * supported, but they must be syntactically valid. Objects in objects * is possible through the namespaces (as static inner class). * * Binding * ---- * The object constraint only includes what has been implemented in the * model (snapshot). An automatic extension of the models at runtime by * the renderer is not detected/supported, but can be implemented in the * application logic - this is a conscious decision! * Case study: * In the markup there is a composite with a property x. There is a * corresponding JavaScript object (model) for the composite but without * the property x. The renderer will mount the composite with the * JavaScript model, the property x will not be found in the model and * will be ignored. At runtime, the model is modified later and the * property x is added. The renderer will not detect the change in the * model and the property x will not be mounted during re-rendering. * Only when the composite is completely removed from the DOM (e.g. by a * condition) and then newly added to the DOM, the property x is also * mounted, because the renderer then uses the current snapshot of the * model and the property x also exists in the model. * * Validation * ---- * Details are described to Composite.validate(selector, lock) * * Synchronization * ---- * View model binding and synchronization assume that a corresponding * static JavaScript object/model exists in the same namespace for the * composite. During synchronization, the element must also exist as a * property in the model. Accepted are properties with a primitive data * type and objects with a property value. The synchronization expects a * positive validation, otherwise it will not be executed. * * Invocation * --- * For events, actions can be implemented in the model. Actions are * static methods in the model whose name begins with 'on' and is * followed by the name (camel case) of the event. As an argument, the * occurring event is passed. The action methods can have a return * value, but do not have to. If their return value is false, the event * and thus the default action of the browser is cancelled. The * invocation expects a positive validation, otherwise it will not be * executed. * * Events * ---- * Composite.EVENT_MOUNT_START * Composite.EVENT_MOUNT_NEXT * Composite.EVENT_MOUNT_END * * Queue and Lock: * ---- * The method used a simple queue and transaction management so that the * concurrent execution of rendering works sequentially in the order of * the method call. * * @param {Element|string} selector DOM element or a string * @param {boolean} lock Unlocking of the model validation * @throws {Error} In the following cases: * - namespace is not valid or is not supported * - namespace cannot be created if it already exists as a method */ mount(selector, lock) { Composite.mount.queue = Composite.mount.queue || []; // The lock locks concurrent mount requests. // Concurrent mounting causes unexpected effects. if (Composite.mount.lock && Composite.mount.lock !== lock) { if (!Composite.mount.queue.includes(selector)) Composite.mount.queue.push(selector); return; } lock = Composite.lock(Composite.mount, selector); try { if (typeof selector === "string") { selector = selector.trim(); if (!selector) return; const nodes = document.querySelectorAll(selector); nodes.forEach((node) => Composite.mount(node, lock.share())); return; } // Exclusive for elements // and without multiple object binding // and script and style elements are not supported if (!(selector instanceof Element) || Composite.mount.queue.includes(selector) || selector.nodeName.match(Composite.PATTERN_ELEMENT_IGNORE)) return; // An element/selector should only be mounted once. Composite.mount.stack = Composite.mount.stack || []; if (Composite.mount.stack.includes(selector)) return; const serial = selector.ordinal(); const object = _render_meta[serial]; // Objects that were not rendered should not be mounted. This // can happen if new DOM elements are created during rendering // that are rendered later. if (!(object instanceof Object)) return; const identifier = object.attributes[Composite.ATTRIBUTE_ID]; // The explicit events are declared by ATTRIBUTE_EVENTS. The // model can, but does not have to, implement the corresponding // method. Explicit events are mainly used to synchronize view // and model and to trigger targets of ATTRIBUTE_RENDER. let events = object.attributes.hasOwnProperty(Composite.ATTRIBUTE_EVENTS) ? object.attributes[Composite.ATTRIBUTE_EVENTS] : ""; events = String(events || ""); events = events.toLowerCase().split(/\s+/); events = events.filter((event, index, array) => Composite.PATTERN_EVENT_FILTER.includes(event) && array.indexOf(event) === index); // There must be a corresponding model. const meta = _mount_lookup(selector); if (meta instanceof Object && identifier) { // The implicit assignment is based on the on-event-methods // implemented in the model. These are determined and added // to the list of events if the events have not yet been // explicitly declared. But this is only useful for elements // with an ID. Since mounting is performed recursively on // the child nodes, it should be prevented that child nodes // are assigned the events of the parents. // Events are possible for composites and their interactive // elements. For this purpose, composites define the scope // with their model. Interactive composite elements are an // object in the model that contains the interaction methods // corresponding to the events. Therefore, the scope of // interactive composite elements shifts from the model to // the according object. In all cases, a name-based // alignment in the model and thus ATTRIBUTE_ID is required // Anonymous interaction elements do not have this alignment // and no scope can be determined. let model = meta.model; if (meta.target !== undefined) if (typeof meta.target === "object") model = meta.target; else model = null; for (let entry in model) if (typeof model[entry] === "function" && entry.match(Composite.PATTERN_EVENT_FUNCTIONS)) { entry = entry.substring(2).toLowerCase(); if (!events.includes(entry)) events.push(entry); } let prototype = model ? Object.getPrototypeOf(model) : null; while (prototype) { Object.getOwnPropertyNames(prototype).forEach(entry => { if (typeof model[entry] === "function" && entry.match(Composite.PATTERN_EVENT_FUNCTIONS)) { entry = entry.substring(2).toLowerCase(); if (!events.includes(entry)) events.push(entry); } }); prototype = Object.getPrototypeOf(prototype); } } // The determined events are registered. Composite.mount.stack.push(selector); events.forEach((event) => { selector.addEventListener(event.toLowerCase(), (event) => { const target = event.currentTarget; const serial = target.ordinal(); const object = _render_meta[serial]; let action = event.type.toLowerCase(); if (!Composite.PATTERN_EVENT_FILTER.includes(action)) return; action = Composite.PATTERN_EVENT_FILTER.indexOf(action); action = Composite.PATTERN_EVENT_NAMES[action]; let result; // Step 1: Validation let valid = Composite.validate(target, false); // There must be a corresponding model. const meta = _mount_lookup(target); if (meta instanceof Object) { let value; if (target instanceof Element) { if (target.tagName.match(/^input$/i) && target.type.match(/^radio|checkbox/i)) value = target.checked; else if (target.tagName.match(/^select/i) && "selectedIndex" in target) value = target.options[target.selectedIndex].value; else if (Composite.ATTRIBUTE_VALUE in target) value = target[Composite.ATTRIBUTE_VALUE]; } // Validation works strictly by default. This means // that the value must explicitly be true and only // then is the input data synchronized with the // model via the HTML elements. This protects // against invalid data in the models which may then // be reflected in the view. If ATTRIBUTE_VALIDATE // is declared as optional, this behaviour can be // specifically deactivated and the input data is // then always synchronized with the model. The // effects of validation are then only optional. if (String(object.attributes[Composite.ATTRIBUTE_VALIDATE]).toLowerCase() === "optional" || valid === true) { // Step 2: Synchronisation // Synchronization expects a data field. It can // be a simple data type or an object with the // property value. Other targets are ignored. // The synchronization expects a successful // validation, otherwise it will not be executed. const accept = (property) => { const type = typeof property; if (property === undefined) return false; if (type === "object" && property === null) return true; return type === "boolean" || type === "number" || type === "string"; }; // A composite is planned as a container for // sub-elements. Theoretically, an input element // can also be a composite and thus both model // and input element / data field. In this case, // a composite can assign a value to itself. if (accept(meta.target)) { meta.target = value; } else if (typeof me