@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
JavaScript
/**
* 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) {