UNPKG

@seanox/aspect-js

Version:

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

992 lines (882 loc) 146 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) 2023 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. * * Markup/DOM, object tree and virtual paths are analog/homogeneous. * Thus virtual paths, object structure in JavaScript (namespace) and the * nesting of the DOM must match. * * @author Seanox Software Solutions * @version 1.6.0 20230402 */ (() => { 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.pathcontext, "/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 namespace */ get ATTRIBUTE_NAMESPACE() {return "namespace";}, /** Constant for attribute notification */ get ATTRIBUTE_NOTIFICATION() {return "notification";}, /** 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 strict */ get ATTRIBUTE_STRICT() {return "strict";}, /** 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|namespace|notification|output|release|render|strict|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) */ get PATTERN_COMPOSITE_ID() {return /^[_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+)?(@[_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 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 context method (render, mound or scan) * @param selector * @return the created lock as meta-object */ 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 event see Composite.EVENT_*** * @param callback callback function * @throws An error occurs 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 event see Composite.EVENT_*** * @param variants up to five additional optional arguments that are * passed as arguments when the callback function is called */ 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 task function to be executed * @param variants up to five additional optional arguments that are * passed as arguments when the callback function is called */ asynchron(task, ...variants) { window.setTimeout((invoke, ...variants) => { invoke(...variants); }, 0, task, ...variants); }, /** * 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 selector selector * @param lock unlocking of the model validation * @return validation result * true, false, undefined/void */ 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 combined with ATTRIBUTE_MESSAGE and // ATTRIBUTE_NOTIFICATION. However, ATTRIBUTE_MESSAGE and // ATTRIBUTE_NOTIFICATION have no effect without ATTRIBUTE_VALIDATE. // The value of the ATTRIBUTE_MESSAGE is used as an error message if // the validation was not successful. To output the error message, // the browser function of the HTML5 form validation is used. This // message is displayed via mouse-over. If ATTRIBUTE_NOTIFICATION is // also used, a value is not expected, the message is output as // overlay/notification/report. 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 (typeof selector.setCustomValidity === "function" && Object.usable(message)) { selector.setCustomValidity(message); if (object.attributes.hasOwnProperty(Composite.ATTRIBUTE_NOTIFICATION) && 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 selector * @param lock * @throws An error occurs 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; // 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) { // 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. // 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]; } // In case of a failed validation, the event and the // default action of the browser will be canceled, // if additionally ATTRIBUTE_STRICT is used. The use // of ATTRIBUTE_STRICT became necessary, because // otherwise invalid inputs are not passed on to the // model, which however it is desirable in some // cases, if in the view also erroneous is to be // reflected. if (!object.attributes.hasOwnProperty(Composite.ATTRIBUTE_STRICT) || 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 meta.target === "object") { if (accept(meta.target["value"])) meta.target["value"] = value; } else if (meta.target === undefined) { if (accept(meta.model["value"])) meta.model["value"] = value; } // Step 3: Invocation // 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 (meta.target && typeof meta.target === "object") model = meta.target; else model = null; // For the event, a corresponding method is // searched in the model that can be called. 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. if (model && typeof model["on" + action] === "function") result = model["on" + action].call(model, event); } } // Step 4: Rendering // Rendering is performed in all cases. When an event // occurs, all elements that correspond to the query // selector rendering are updated. let events = object.attributes.hasOwnProperty(Composite.ATTRIBUTE_EVENTS) ? object.attributes[Composite.ATTRIBUTE_EVENTS] : ""; events = String(events || ""); events = events.toLowerCase().split(/\s+/); if (events.includes(action.toLowerCase())) { let render = object.attributes.hasOwnProperty(Composite.ATTRIBUTE_RENDER) ? object.attributes[Composite.ATTRIBUTE_RENDER] : ""; render = String(render || ""); if ((render || "").match(Composite.PATTERN_EXPRESSION_CONTAINS)) render = Expression.eval(serial + ":" + Composite.ATTRIBUTE_RENDER, render); Composite.render(render); } if (meta instanceof Object) {