const UpdaterExcludedNodes = new Set([
'#document-fragment',
'SCRIPT', 'STYLE', 'LINK', 'NOSCRIPT',
'META', 'BASE', 'TITLE',
'TEMPLATE', 'SLOT',
'IFRAME', 'OBJECT', 'EMBED',
'SOURCE', 'TRACK', 'PORTAL',
'CANVAS', 'SVG', 'MATH', 'svg', 'math',
'semantics', 'annotation', 'annotation-xml',
'script', 'style', 'link', 'desc', 'title',
'metadata', 'defs', 'symbol', 'clipPath',
'mask', 'pattern', 'foreignObject', 'g',
'path', 'rect', 'circle', 'line', 'polygon',
'use', 'text', 'polyline', 'ellipse', 'image',
'tspan', 'linearGradient', 'radialGradient',
'stop', 'marker', 'filter',
]);
function UpdaterStart(targetNode, handlers) {
function Updater(mutations) {
for (const mutation of mutations) {
let target = mutation.target;
switch (mutation.type) {
case 'attributes': {
if (!IncludedNodes.has(target)) {
break;
}
const attributeName = mutation.attributeName;
const nodeAttributes = IncludedAttrs.get(target);
const oldValue = nodeAttributes[attributeName];
const newValue = target.getAttribute(attributeName);
if (oldValue === newValue) {
break;
}
if (oldValue === undefined) {
nodeAttributes[attributeName] = newValue;
handlers.attributes.added(target, attributeName, newValue, oldValue);
}
else if (newValue === null) {
Reflect.deleteProperty(nodeAttributes, attributeName);
handlers.attributes.removed(target, attributeName, newValue, oldValue);
}
else {
nodeAttributes[attributeName] = newValue;
handlers.attributes.modified(target, attributeName, newValue, oldValue);
}
break;
}
case 'characterData': {
if (!IncludedNodes.has(target)) {
break;
}
let oldValue = IncludedCommentsAndTexts.get(target);
if (oldValue === target.data || !handlers.characterData[target.nodeType]) {
break;
}
IncludedCommentsAndTexts.set(target, target.data);
handlers.characterData[target.nodeType].modified(target, target.data, oldValue);
break;
}
case 'childList': {
if (UpdaterExcludedNodes.has(target.nodeName) || !IncludedNodes.has(target)) {
break;
}
if (mutation.removedNodes.length) {
ProcessNodes(target, mutation.removedNodes, 'removed');
}
if (mutation.addedNodes.length) {
ProcessNodes(target, mutation.addedNodes, 'added');
}
break;
}
}
}
}
function ProcessNodes(parentNode, nodes, action) {
for (let i = 0; i < nodes.length; i++) {
ProcessTree(parentNode, nodes[i], action);
}
}
function ProcessTree(parentNode, rootNode, action) {
const added = (action === 'added');
if (UpdaterExcludedNodes.has(rootNode.nodeName) ||
(added && IncludedNodes.get(rootNode) === parentNode) ||
(!added && !IncludedNodes.has(rootNode))) {
return;
}
const type = rootNode.nodeType;
switch (type) {
case Node.COMMENT_NODE:
case Node.TEXT_NODE:
if (added) {
IncludedNodes.set(rootNode, parentNode);
IncludedCommentsAndTexts.set(rootNode, rootNode.data);
handlers.childList[type][action](rootNode, parentNode, rootNode.data);
}
else {
handlers.childList[type][action](rootNode, parentNode, IncludedCommentsAndTexts.get(rootNode));
IncludedCommentsAndTexts.delete(rootNode);
IncludedNodes.delete(rootNode);
}
break;
case Node.ELEMENT_NODE:
const hasAttrs = handlers.attributes && rootNode.hasAttributes();
if (added) {
IncludedNodes.set(rootNode, parentNode);
handlers.childList[type][action](rootNode, parentNode);
let attrs = Object.create(null);
IncludedAttrs.set(rootNode, attrs);
if (hasAttrs) {
for (const attr of rootNode.attributes) {
attrs[attr.name] = attr.value;
handlers.attributes.added(rootNode, attr.name, attr.value, undefined);
}
}
}
try {
let child = rootNode.firstChild;
while (child) {
const current = child.nextSibling;
ProcessTree(rootNode, child, action);
child = current;
}
}
catch (e) {
if (e instanceof RangeError) {
throw new Error('Updater > ProcessTree: ' + e.message);
}
throw e;
}
if (!added) {
if (hasAttrs) {
for (const attr of rootNode.attributes) {
handlers.attributes.removed(rootNode, attr.name, null, attr.value);
}
}
IncludedAttrs.delete(rootNode);
handlers.childList[type][action](rootNode, parentNode);
IncludedNodes.delete(rootNode);
}
}
}
const IncludedCommentsAndTexts = new WeakMap();
const IncludedAttrs = new WeakMap();
const IncludedNodes = new WeakMap();
try {
const ObserverConfig = Object.create(null);
ObserverConfig.subtree = true;
try {
ObserverConfig.attributes = !!handlers.attributes;
if (ObserverConfig.attributes) {
if(!(handlers.attributes.added && handlers.attributes.removed && handlers.attributes.modified)) {
throw new Error('attributes: added, removed, modified required functions.');
}
}
ObserverConfig.characterData = !!handlers.characterData;
if (ObserverConfig.characterData) {
if (!(handlers.characterData[Node.COMMENT_NODE].modified && handlers.characterData[Node.TEXT_NODE].modified)) {
throw new Error('characterData: [Node.COMMENT_NODE].modified && [Node.TEXT_NODE].modified required functions.');
}
}
ObserverConfig.childList = !!handlers.childList;
if (ObserverConfig.childList) {
if (!(handlers.childList[Node.COMMENT_NODE].added && handlers.childList[Node.COMMENT_NODE].removed &&
handlers.childList[Node.TEXT_NODE].added && handlers.childList[Node.TEXT_NODE].removed &&
handlers.childList[Node.ELEMENT_NODE].added && handlers.childList[Node.ELEMENT_NODE].removed)
) {
throw new Error('childList: [Node.ELEMENT_NODE], [Node.TEXT_NODE], [Node.COMMENT_NODE] each with added && removed required functions.');
}
}
}
catch (e) {
throw new Error('ObserverConfig: ' + e.message);
}
const Observer = new MutationObserver(Updater);
Observer.observe(targetNode, ObserverConfig);
if (targetNode.nodeType === Node.DOCUMENT_NODE) {
IncludedNodes.set(targetNode, null);
if (targetNode.documentElement) {
ProcessTree(targetNode, targetNode.documentElement, 'added');
}
return Observer;
}
ProcessTree(targetNode.parentNode, targetNode, 'added');
return Observer;
}
catch (e) {
throw new Error('UpdaterStart > Observer > Initialization: ' + e.message);
}
}
JavaScript
const XVars_prefix = '${';
const XVars_postfix = '}';
const XVarsNamesRE = /[^a-z_0-9]/i;
const XVarsNodes = new WeakMap();
const XVarsSuscriptors = new Map();
const XElementDataStr = 'data-x';
const XElements = new WeakSet();
const UpdaterObserver = UpdaterStart(document, {
childList: {
[Node.ELEMENT_NODE]: {
added: (elementNode, parentNode) => {
const hasX = elementNode.hasAttribute(XElementDataStr);
if (hasX) {
console.log('X ELEMENT ADDED:', elementNode);
XElements.add(elementNode);
}
},
removed: (elementNode, parentNode) => {
if (XElements.has(elementNode)) {
XElements.delete(elementNode);
console.log('X ELEMENT REMOVED:', elementNode);
}
}
},
[Node.COMMENT_NODE]: {
added: (commentNode, parentNode, newValue) => {
// console.log('COMMENT ADD:', commentNode, parentNode, newValue);
},
removed: (commentNode, parentNode, oldValue) => {
// console.log('COMMENT DEL:', commentNode, parentNode, oldValue);
}
},
[Node.TEXT_NODE]: {
added: (textNode, parentNode, newValue) => {
if (newValue.trim() === '') { return; }
const XParent = parentNode.closest('[' + XElementDataStr + ']');
if (XParent && XElements.has(XParent) && newValue.includes(XVars_prefix)) {
const varsList = [];
const varsTemplate = [];
const vars = newValue
.split(XVars_prefix)
.reduce((array, str) => {
if (str.includes(XVars_postfix)){
let varPos = str.indexOf(XVars_postfix) + 1;
let varName = str.slice(0, varPos - 1);
if (!XVarsNamesRE.test(varName)) {
varsList.push(varName);
varsTemplate.push(() => { return XState[varName]; });
varsTemplate.push(str.slice(varPos));
return array.concat(str);
}
}
varsTemplate.push(str);
return array.concat(str);
}, []);
if (varsList.length) {
for (let varName of varsList) {
if (!XVarsSuscriptors.has(varName)) {
XVarsSuscriptors.set(varName, new Set());
}
const xVarSuscriptor = XVarsSuscriptors.get(varName);
if (!xVarSuscriptor.has(textNode)){
xVarSuscriptor.add(textNode);
}
if (!XVarsNodes.has(textNode)) {
XVarsNodes.set(textNode, varsTemplate);
}
}
console.log('X TEXT ADD:', varsList, varsTemplate);
for (let varName of varsList) {
XState[varName] = XState[varName] || '';
}
}
}
},
removed: (textNode, parentNode, oldValue) => {
// console.log('TEXT DEL:', textNode, parentNode, oldValue);
for (const [varName, nodeSet] of XVarsSuscriptors.entries()) {
nodeSet.delete(textNode);
if (nodeSet.size === 0) {
XVarsSuscriptors.delete(varName);
}
}
}
}
}
});
let XStateBatchState = false;
let XStateBatch = new Set();
const XState = State({
onSet: (mutation) => {
const xVarName = mutation.path[0];
if (XVarsSuscriptors.has(xVarName)) {
const subs = XVarsSuscriptors.get(xVarName);
if (subs) {
for (const node of subs) {
XStateBatch.add(node);
}
}
}
XStateBatchExecution();
},
onDelete: (mutation) => {
}
});
function XStateBatchExecution() {
if (XStateBatchState) {
return;
}
XStateBatchState = true;
requestAnimationFrame(() => {
XStateBatchState = false;
for (const suscriptor of XStateBatch.values()) {
if (XVarsNodes.has(suscriptor)) {
const varValue = XVarsNodes.get(suscriptor).map((varPart) => (typeof varPart === 'function' ? varPart() : varPart)).join('');
switch (suscriptor.nodeType) {
case Node.TEXT_NODE:
suscriptor.data = varValue;
break;
}
}
}
XStateBatch.clear();
});
}
HTML
<head>
<!-- [Previous STATE Minimal Implementation JavaScript Code File] -->
<script src="./test/State.js"></script>
<!-- [Previous UPDATER Minimal Implementation JavaScript Code File] -->
<script src="./test/Updater.js"></script>
<!-- [Previous UPDATER Usage Example JavaScript Code] -->
<script src="./test/Updater.usage.js"></script>
<script>
// TESTING REACTIVE VALUES
setInterval(() => {
XState['VALUE_1'] = Math.round(Math.random() * Date.now());
XState['VALUE_2'] = Math.round(Math.random() * Date.now());
XState['VALUE_3'] = Math.round(Math.random() * Date.now());
}, 567);
</script>
</head>
<body>
<div data-x>
<p><b>Reactive value:</b> ${VALUE_1}, ${VALUE_2}</p>
<p><b>Another reactive value:</b> ${VALUE_3}</p>
</div>
</body>