Skip to content
Snippets Groups Projects
Commit 61019361 authored by Volker Schukai's avatar Volker Schukai :alien:
Browse files

#29

parent f6a359d8
No related branches found
No related tags found
No related merge requests found
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
/** Monster 1.9.0, © 2021 schukai GmbH, Released under the AGPL 3.0 License. */
'use strict';import{Pipe}from"../data/pipe.js";import{Base,Monster}from"../types/base.js";import{isInstance,isArray}from"../types/is.js";import{Observer}from"../types/observer.js";import{ProxyObserver}from"../types/proxyobserver.js";import{validateInstance}from"../types/validate.js";import{clone}from"../util/clone.js";import{Diff}from"../data/diff.js";import{ATTRIBUTE_UPDATER_REPLACE,ATTRIBUTE_UPDATER_ATTRIBUTES,ATTRIBUTE_UPDATER_INSERT,ATTRIBUTE_UPDATER_REMOVE,ATTRIBUTE_UPDATER_INSERT_REFERENCE}from"../dom/constants.js";import{getDocument}from"./util.js";class Updater extends Base{constructor(element,subject){super();this.element=validateInstance(element,HTMLElement);if(!isInstance(subject,ProxyObserver)){subject=new ProxyObserver(subject)}this.last={};this.callbacks=new Map;this.subject=subject.attachObserver(new Observer(()=>{const s=this.subject.getRealSubject();const diff=Diff(this.last,s);this.last=clone(s);for(const[,change]of Object.entries(diff)){removeElement.call(this,change);insertElement.call(this,change);updateContent.call(this,change);updateAttributes.call(this,change)}}))}run(){this.last={};return this.subject.notifyObservers()}getSubject(){return this.subject.getSubject()}setCallback(name,callback){this.callbacks.set(name,callback);return this}}function removeElement(change){for(const[,element]of this.element.querySelectorAll(":scope ["+ATTRIBUTE_UPDATER_REMOVE+"]").entries()){element.parentNode.removeChild(element)}return this}function insertElement(change){const self=this;const subject=self.subject.getRealSubject();const document=getDocument();let mem=new WeakSet;let wd=0;while(true){let found=false;wd++;let p=clone(change?.["path"]);if(!isArray(p))return this;while(p.length>0){const current=p.join(".");const list=this.element.querySelectorAll(":scope ["+ATTRIBUTE_UPDATER_INSERT+"*=\"path:"+current+"\"]").entries();for(const[,containerElement]of list){if(mem.has(containerElement))continue;mem.add(containerElement);found=true;const attributes=containerElement.getAttribute(ATTRIBUTE_UPDATER_INSERT);let def=attributes.trim();let i=def.indexOf(" ");let key=def.substr(0,i).trim();let refPrefix=key+"-";let cmd=def.substr(i).trim();if(cmd.indexOf("|")>0){throw new Error("pipes are not allowed when cloning a node.")}let pipe=new Pipe(cmd);this.callbacks.forEach((f,n)=>{pipe.setCallback(n,f)});let value=pipe.run(subject);let dataPath=cmd.split(":").pop();let insertPoint;if(containerElement.hasChildNodes()){insertPoint=containerElement.lastChild}if(!isArray(value)){throw new Error("the value is not iterable")}let available=new Set;for(const[_i,obj]of Object.entries(value)){let ref=refPrefix+_i;let currentPath=dataPath+"."+_i;available.add(ref);let refElement=containerElement.querySelector("["+ATTRIBUTE_UPDATER_INSERT_REFERENCE+"=\""+ref+"\"]");if(refElement instanceof HTMLElement){insertPoint=refElement;continue}appendNewDocumentFragment(containerElement,key,ref,currentPath)}let nodes=containerElement.querySelectorAll("["+ATTRIBUTE_UPDATER_INSERT_REFERENCE+"*=\""+refPrefix+"\"]");for(const[,node]of Object.entries(nodes)){if(!available.has(node.getAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE))){containerElement.removeChild(node)}}}p.pop()}if(found===false)break;if(wd++>200){throw new Error("the maximum depth for the recursion is reached.")};}}function appendNewDocumentFragment(container,key,ref,path){let template=getDocument().querySelector("template#"+key);if(!(template instanceof HTMLTemplateElement)){throw new Error("no template was found with the specified key "+key)}let nodes=template.content.cloneNode(true);for(const[,node]of Object.entries(nodes.childNodes)){if(node instanceof HTMLElement){node.setAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE,ref);if(node.hasAttribute(ATTRIBUTE_UPDATER_REPLACE)){let value=node.getAttribute(ATTRIBUTE_UPDATER_REPLACE);node.setAttribute(ATTRIBUTE_UPDATER_REPLACE,value.replace("path:"+key,"path:"+path))}}container.appendChild(node)}}function updateContent(change){const self=this;const subject=self.subject.getRealSubject();let p=clone(change?.["path"]);runUpdateContent.call(this,this.element,p,subject);return this}function runUpdateContent(container,parts,subject){if(!isArray(parts))return;parts=clone(parts);let mem=new WeakSet;while(parts.length>0){const current=parts.join(".");parts.pop();for(const[,element]of container.querySelectorAll(":scope ["+ATTRIBUTE_UPDATER_REPLACE+"^=\"path:"+current+"\"], ["+ATTRIBUTE_UPDATER_REPLACE+"^=\"static:\"]").entries()){if(mem.has(element))continue;mem.add(element);const attributes=element.getAttribute(ATTRIBUTE_UPDATER_REPLACE);let cmd=attributes.trim();let pipe=new Pipe(cmd);this.callbacks.forEach((f,n)=>{pipe.setCallback(n,f)});let value=pipe.run(subject);if(value instanceof HTMLElement){while(element.firstChild){element.removeChild(element.firstChild)}element.addNode(value)}else{element.innerHTML=value}}}}function updateAttributes(change){const self=this;const subject=self.subject.getRealSubject();let p=clone(change?.["path"]);runUpdateAttributes(this.element,p,subject);return this}function runUpdateAttributes(container,parts,subject){if(!isArray(parts))return;parts=clone(parts);let mem=new WeakSet;while(parts.length>0){const current=parts.join(".");parts.pop();for(const[,element]of container.querySelectorAll(":scope ["+ATTRIBUTE_UPDATER_ATTRIBUTES+"*=\"path:"+current+"\"]").entries()){if(mem.has(element))continue;mem.add(element);const attributes=element.getAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES);for(let[,def]of Object.entries(attributes.split(","))){def=def.trim();let i=def.indexOf(" ");let name=def.substr(0,i).trim();let cmd=def.substr(i).trim();let value=new Pipe(cmd).run(subject);if(element.getAttribute(name)!==value){element.setAttribute(name,value)}}}}}Monster.assignToNamespace("Monster.DOM",Updater);export{Monster,Updater};
'use strict';import{Pipe}from"../data/pipe.js";import{Base,Monster}from"../types/base.js";import{isInstance,isArray}from"../types/is.js";import{Observer}from"../types/observer.js";import{ProxyObserver}from"../types/proxyobserver.js";import{validateInstance}from"../types/validate.js";import{clone}from"../util/clone.js";import{Diff}from"../data/diff.js";import{ATTRIBUTE_UPDATER_REPLACE,ATTRIBUTE_UPDATER_ATTRIBUTES,ATTRIBUTE_UPDATER_INSERT,ATTRIBUTE_UPDATER_REMOVE,ATTRIBUTE_UPDATER_INSERT_REFERENCE}from"../dom/constants.js";import{getDocument}from"./util.js";class Updater extends Base{constructor(element,subject){super();this.element=validateInstance(element,HTMLElement);if(subject===undefined)subject={};if(!isInstance(subject,ProxyObserver)){subject=new ProxyObserver(subject)}this.last={};this.callbacks=new Map;this.subject=subject.attachObserver(new Observer(()=>{const s=this.subject.getRealSubject();const diff=Diff(this.last,s);this.last=clone(s);for(const[,change]of Object.entries(diff)){removeElement.call(this,change);insertElement.call(this,change);updateContent.call(this,change);updateAttributes.call(this,change)}}))}run(){this.last={"__init__":true};return this.subject.notifyObservers()}getSubject(){return this.subject.getSubject()}setCallback(name,callback){this.callbacks.set(name,callback);return this}}function removeElement(change){for(const[,element]of this.element.querySelectorAll(":scope ["+ATTRIBUTE_UPDATER_REMOVE+"]").entries()){element.parentNode.removeChild(element)}return this}function insertElement(change){const self=this;const subject=self.subject.getRealSubject();const document=getDocument();let mem=new WeakSet;let wd=0;while(true){let found=false;wd++;let p=clone(change?.["path"]);if(!isArray(p))return this;while(p.length>0){const current=p.join(".");const list=this.element.querySelectorAll(":scope ["+ATTRIBUTE_UPDATER_INSERT+"*=\"path:"+current+"\"]").entries();for(const[,containerElement]of list){if(mem.has(containerElement))continue;mem.add(containerElement);found=true;const attributes=containerElement.getAttribute(ATTRIBUTE_UPDATER_INSERT);let def=attributes.trim();let i=def.indexOf(" ");let key=def.substr(0,i).trim();let refPrefix=key+"-";let cmd=def.substr(i).trim();if(cmd.indexOf("|")>0){throw new Error("pipes are not allowed when cloning a node.")}let pipe=new Pipe(cmd);this.callbacks.forEach((f,n)=>{pipe.setCallback(n,f)});let value=pipe.run(subject);let dataPath=cmd.split(":").pop();let insertPoint;if(containerElement.hasChildNodes()){insertPoint=containerElement.lastChild}if(!isArray(value)){throw new Error("the value is not iterable")}let available=new Set;for(const[_i,obj]of Object.entries(value)){let ref=refPrefix+_i;let currentPath=dataPath+"."+_i;available.add(ref);let refElement=containerElement.querySelector("["+ATTRIBUTE_UPDATER_INSERT_REFERENCE+"=\""+ref+"\"]");if(refElement instanceof HTMLElement){insertPoint=refElement;continue}appendNewDocumentFragment(containerElement,key,ref,currentPath)}let nodes=containerElement.querySelectorAll("["+ATTRIBUTE_UPDATER_INSERT_REFERENCE+"*=\""+refPrefix+"\"]");for(const[,node]of Object.entries(nodes)){if(!available.has(node.getAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE))){containerElement.removeChild(node)}}}p.pop()}if(found===false)break;if(wd++>200){throw new Error("the maximum depth for the recursion is reached.")}}}function appendNewDocumentFragment(container,key,ref,path){let template=getDocument().querySelector("template#"+key);if(!(template instanceof HTMLTemplateElement)){throw new Error("no template was found with the specified key "+key)}let nodes=template.content.cloneNode(true);for(const[,node]of Object.entries(nodes.childNodes)){if(node instanceof HTMLElement){node.setAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE,ref);if(node.hasAttribute(ATTRIBUTE_UPDATER_REPLACE)){let value=node.getAttribute(ATTRIBUTE_UPDATER_REPLACE);node.setAttribute(ATTRIBUTE_UPDATER_REPLACE,value.replace("path:"+key,"path:"+path))}}container.appendChild(node)}}function updateContent(change){const self=this;const subject=self.subject.getRealSubject();let p=clone(change?.["path"]);runUpdateContent.call(this,this.element,p,subject);return this}function runUpdateContent(container,parts,subject){if(!isArray(parts))return;parts=clone(parts);let mem=new WeakSet;while(parts.length>0){const current=parts.join(".");parts.pop();for(const[,element]of container.querySelectorAll(":scope ["+ATTRIBUTE_UPDATER_REPLACE+"^=\"path:"+current+"\"], ["+ATTRIBUTE_UPDATER_REPLACE+"^=\"static:\"]").entries()){if(mem.has(element))continue;mem.add(element);const attributes=element.getAttribute(ATTRIBUTE_UPDATER_REPLACE);let cmd=attributes.trim();let pipe=new Pipe(cmd);this.callbacks.forEach((f,n)=>{pipe.setCallback(n,f)});let value=pipe.run(subject);if(value instanceof HTMLElement){while(element.firstChild){element.removeChild(element.firstChild)}element.addNode(value)}else{element.innerHTML=value}}}}function updateAttributes(change){const self=this;const subject=self.subject.getRealSubject();let p=clone(change?.["path"]);runUpdateAttributes(this.element,p,subject);return this}function runUpdateAttributes(container,parts,subject){if(!isArray(parts))return;parts=clone(parts);let mem=new WeakSet;while(parts.length>0){const current=parts.join(".");parts.pop();for(const[,element]of container.querySelectorAll(":scope ["+ATTRIBUTE_UPDATER_ATTRIBUTES+"*=\"path:"+current+"\"]").entries()){if(mem.has(element))continue;mem.add(element);const attributes=element.getAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES);for(let[,def]of Object.entries(attributes.split(","))){def=def.trim();let i=def.indexOf(" ");let name=def.substr(0,i).trim();let cmd=def.substr(i).trim();let value=new Pipe(cmd).run(subject);if(element.getAttribute(name)!==value){element.setAttribute(name,value)}}}}}Monster.assignToNamespace("Monster.DOM",Updater);export{Monster,Updater};
......@@ -86,7 +86,7 @@ class Updater extends Base {
/**
* @since 1.8.0
* @param {HTMLElement} element
* @param {object|ProxyObserver} subject
* @param {object|ProxyObserver|undefined} subject
* @throws {TypeError} value is not a object
* @throws {TypeError} value is not an instance of HTMLElement
*/
......@@ -98,6 +98,7 @@ class Updater extends Base {
*/
this.element = validateInstance(element, HTMLElement);
if(subject===undefined) subject = {}
if (!isInstance(subject, ProxyObserver)) {
subject = new ProxyObserver(subject);
}
......
......@@ -78,10 +78,10 @@ describe('DOM', function () {
expect(() => new Updater()).to.throw(TypeError)
})
it('should throw value is not an instance of HTMLElement Error', function () {
it('should throw value is wrong', function () {
let element = document.getElementById('test1')
expect(() => new Updater(element)).to.throw(TypeError)
expect(() => new Updater(element,null)).to.throw(TypeError)
})
it('should throw Error: the value is not iterable', function (done) {
......
......@@ -40,6 +40,7 @@ setTimeout(function(){
subject['headline'] = "Hello!"
},1000);
```
We have seen how we can change the content of an htm element. now let's look at what options are available.
......@@ -50,8 +51,26 @@ We have seen how we can change the content of an htm element. now let's look at
The simplest manipulation is to replace the content of a HTMLElement.
To do this, simply use the `data-monster-replace` attribute (see example).
The syntax is quite simple. The result of the attribute pipe is inserted as content of the
HTMLElement. For the processing the [Pipe](Monster_Data.Pipe.html)<!-- @IGNORE PREVIOUS: link --> and [Transformer class](Monster_Data.Transformer.html)<!-- @IGNORE PREVIOUS: link -->
is used.
If, for example, you have an object `x` with the structure listed below and want to insert the value of the key b, you write: `path:a.b`.
The pipe can then be used to apply operators. For example, `tolower` can be used to convert everything to lowercase.
```
<div data-monster-replace="static:hello"></div>
let x = {
a: {
b: "EXAMPLE"
}
}
```
this is how it looks as an attribute:
```
<div data-monster-replace="static:HELLO | tolower"></div>
```
The result is then the following html:
......@@ -62,5 +81,118 @@ The result is then the following html:
A full example looks like this:
```
import {Updater} from 'https://cdn.jsdelivr.net/npm/@schukai/monster@1.9.0/dist/modules/dom/updater.js';
const body = document.querySelector('body');
const headline = document.createElement('h1');
headline.setAttribute('data-monster-replace','static:hello')
body.appendChild(headline);
// result in ↦ <div data-monster-replace="static:hello"></div>
new Updater(body).run();
```
## Attributes
Attributes can be added via the `data-monster-attributes` attribute. The syntax is attribute name followed by
a space and the desired pipe definition.
```
<div data-monster-attributes="id static:myid">hello</div>
```
The result is then the following html:
```
<div id="myid" data-monster-attributes="id static:myid">hello</div>
```
Multiple attributes can be separated by commas.
```
<div data-monster-attributes="id static:myid, class static:myclass">hello</div>
```
## Remove
The `data-monster-remove` attribute can be used to remove html elements. it is important to
note that this cannot be undone. Once removed, nodes will not be reinserted.
This tag is removed via the updater
```
<div data-monster-remove></div>
```
## Insert
The strongest feature is adding elements to a node. For this you need a
template and the `data-monster-instert` attribute.
The syntax of the attribute is first an id followed by a space. This is then followed by the pipe command.
the values for the corresponding data must be available as an array.
```
let obj = {
a: [
{"id": 1},
{"id": 2},
{"id": 3},
{"id": 4}
]
};
```
here is a full example:
```
const body = document.querySelector('body');
const li = document.createElement('li');
li.innerHTML="-/-";
li.setAttribute('data-monster-replace','path:myid | index:id | tostring | prefix:x');
const template = document.createElement('template');
template.setAttribute('id','myid');
template.content.appendChild(li);
body.appendChild(template);
const list = document.createElement('ul');
list.setAttribute('data-monster-insert', 'myid path:a')
body.appendChild(list);
let obj = {
a: [
{"id": 1},
{"id": 2},
{"id": 3},
{"id": 4}
]
};
const updater = new Updater(body, obj)
updater.run();
```
the result will be
```
<ul data-monster-insert="myid path:a">
<li data-monster-replace="path:a.0 | index:id | tostring | prefix:x" data-monster-insert-reference="myid-0">x1</li>
<li data-monster-replace="path:a.1 | index:id | tostring | prefix:x" data-monster-insert-reference="myid-1">x2</li>
<li data-monster-replace="path:a.2 | index:id | tostring | prefix:x" data-monster-insert-reference="myid-2">x3</li>
<li data-monster-replace="path:a.3 | index:id | tostring | prefix:x" data-monster-insert-reference="myid-3">x4</li>
</ul>
```
You can easily add and delete values to the array. The DOM will be adjusted accordingly.
The attribute `data-monster-insert-reference` identifies if the entry already exists.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment