@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
292 lines (244 loc) • 13.5 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
* ----
* A reactivity system or here called reactivity rendering is a mechanism which
* automatically keeps in sync a data source (model) with a data representation
* (view) layer. Every time the model changes, the view is partially
* re-rendered to reflect the changes.
*
* The mechanism is based on notifications that arise from setting and getting
* from the model as a data source. Which is supported by the proxy object in
* JavaScript and its events can then be used to determine which elements/nodes
* in the DOM consume what data from the model and need to be updated when
* changes are made.
*
* Reactivity rendering is implemented as an optional module and uses the
* available API.
*
* Reactive works permanently recursively on all objects, in all levels of a
* model and also on the objects that are added later as values, even if these
* objects do not explicitly use the Reactive.
*
* Object and model are decoupled by Reactive. The implementation uses free
* (unbound) proxies for this purpose. These proxies reference an object but are
* not bound to an object level in the object tree and they synchronize the data
* bidirectionally. Managed these proxies are managed with a weak map where the
* object is the key and the garbage collection can dispose of this objects with
* associated proxies when not in use.
*
* @author Seanox Software Solutions
* @version 1.6.0 20230317
*/
(() => {
compliant("Reactive");
compliant(null, window.Reactive = (object) => {
if (typeof object !== "object")
throw new TypeError("Not supported data type: " + typeof object);
if (object === "object")
throw new TypeError("Not supported data type: null");
return _reactive(object);
});
let _selector = null;
Composite.listen(Composite.EVENT_RENDER_START, (event, selector) =>
_selector = selector);
Composite.listen(Composite.EVENT_RENDER_NEXT, (event, selector) =>
_selector = selector);
Composite.listen(Composite.EVENT_RENDER_END, (event, selector) =>
_selector = null);
/**
* Enhancement of the JavaScript API
* Adds a function to create a reactive object to an object instance. If it
* is already a reactive object, the reference of the instance is returned.
*/
compliant("Object.prototype.reactive");
compliant(null, Object.prototype.reactive = function() {
return _reactive(this);
});
/**
* Proxy is implemented exotically, cannot be inherited and has no
* prototype. Therefore, this unconventional way with a secret simulated
* property that is used as an indicator for existing reactive objects
* instances. The value is not programmatically constant, instead it is
* defined with the start of the application.
* https://stackoverflow.com/questions/37714787/can-i-extend-proxy-with-an-es2015-class
*/
const _secret = Math.serial();
/**
* Weak map with the assignment of objects to proxies. The object is the key
* and the proxy is the value. A feature of WeakMap is that when the key is
* purged from the garbage collection, the value and thus the proxy is also
* purged. Thus, this should be an efficient way to manage unbound proxies.
*/
const _register = new WeakMap();
const _reactive = (object) => {
if (typeof object !== "object"
|| object === null)
return object;
// Proxy remains proxy
if (object[_secret] !== undefined)
return object;
// For all objects, a proxy must be created. Also for proxies, even if
// proxy in proxy is prevented. Not internally, so it works recursively.
// Endless loops are prevented with the register.
if (_register.has(object))
return _register.get(object);
const proxy = new Proxy(object, {
notifications: new Map(),
cache: new Map(),
get(target, key) {
try {
// Proxy is implemented exotically, cannot be inherited and
// has no prototype. Therefore, this unconventional way with
// a secret simulated property that is used as an indicator
// for existing reactive object instances and also contains
// a reference to the original object.
if (key === _secret)
return target;
let value;
// During analysis, getters must be invoked via the proxy to
// identify the final targets behind the getter.
if (_selector) {
const descriptor = Object.getOwnPropertyDescriptor(target, key);
if (descriptor && typeof descriptor.get === "function")
value = descriptor.get.call(proxy);
else value = target[key];
} else value = target[key];
// Proxies are only used for objects, other data types are
// returned directly.
if (typeof value !== "object"
|| value === null)
return value;
// Proxy remains proxy
if (value[_secret] !== undefined)
return value;
// A proxy always returns proxies for objects. To decouple
// object, proxy and view and to avoid reference to object
// tree/level, loose proxies are used. The mapping is based
// only on objects not on object level via the register.
if (_register.has(value))
return _register.get(value)
return _reactive(value);
// To be economical with resources, proxies are not created
// for objects immediately, but only when they are requested
// via getter. Therefore, the properties for an object are
// not analyzed recursively.
} finally {
// The registration is delayed so that the getting of values
// does not block unnecessarily.
Composite.asynchron((selector, target, key, notifications) => {
// Registration is performed only during rendering and
// if the key exists in the object.
if (selector === null
|| !target.hasOwnProperty(key))
return;
// Special for elements with attribute iterate. For
// these, the highest parent element with the attribute
// iterate is searched for and registered as the
// recipient. Why -- Iterate provides temporary
// variables which can be used in the enclosed markup.
// If these places are registered as recipients, these
// temporary variables cannot be accessed later in the
// expressions, which causes errors because the
// temporary variables no longer exist. Since element
// with the attribute iterate can be nested and the
// expression can be based on a parent one, the topmost
// one is searched for.
for (let node = selector; node.parentNode; node = node.parentNode) {
const meta = (Composite.render.meta || [])[node.ordinal()] || {};
if (meta.attributes && meta.attributes.hasOwnProperty(Composite.ATTRIBUTE_ITERATE))
selector = node;
}
const recipients = notifications.get(key) || new Map();
// If the selector as the current rendered element is
// already registered as a recipient, then the
// registration can be canceled.
if (recipients.has(selector.ordinal()))
return;
for (const recipient of recipients.values()) {
// If the selector as the current rendered element
// is already contained in a recipient as the
// parent, the selector as a recipient can be
// ignored, because the rendering is initiated by
// the parent and includes the selector as a child.
if (recipient.contains !== undefined
&& recipient.contains(selector))
return;
// If the selector as current rendered element
// contains a recipient as parent, the recipient can
// be removed, because the selector element will
// initiate rendering as parent in the future and
// the existing recipient will be rendered as child
// automatically.
if (selector.contains !== undefined
&& selector.contains(recipient))
recipients.delete(recipient.ordinal());
}
recipients.set(selector.ordinal(), selector);
notifications.set(key, recipients);
}, _selector, target, key, this.notifications);
}
},
set(target, key, value) {
// To decouple object, proxy and view, the original objects are
// always used as value and never the proxies.
if (typeof value === "object"
&& value !== null
&& value[_secret] !== undefined)
value = value[_secret];
// To be economical with resources, proxies are not created for
// objects immediately, but only when they are explicitly
// requested via getter.
try {return target[key] = value;
} finally {
// Unwanted recursions due to repeated value assignments:
// a = a / a = c = b = a must be avoided so that no infinite
// render cycle is initiated.
if (this.cache.get(key) === value)
return;
this.cache.set(key, value);
// The registration is delayed so that the setting of values
// does not block unnecessarily.
Composite.asynchron((selector, target, key, notifications) => {
// Update only if the key exists in the object.
// Recursions during rendering are prevented via the
// queue and the lock in during rendering.
if (!target.hasOwnProperty(key))
return;
const recipients = this.notifications.get(key) || new Map();
for (const recipient of recipients.values()) {
// If the recipient is no longer included in the DOM
// and so it can be removed this case.
if (!document.body.contains(recipient))
recipients.delete(recipient.ordinal());
else Composite.render(recipient);
}
}, _selector, target, key, this.notifications);
}
}
});
// On the one hand, the register manages the unbound proxies, on the
// other hand, it protects against endless recursions.
_register.set(object, proxy);
return proxy;
};
})();