Skip to content

Commit

Permalink
DOM diffing
Browse files Browse the repository at this point in the history
  • Loading branch information
CMEONE committed Mar 17, 2021
1 parent 19a3e1d commit e2618a7
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 11 deletions.
53 changes: 48 additions & 5 deletions example/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ class Counter extends tApp.Component {
for(let i = 0; i < this.children.length; i++) {
this.children[i].destroy();
}
return (`
<div>
return (`<div>
[[
CounterButton
{
Expand All @@ -66,8 +65,7 @@ class Counter extends tApp.Component {
incrementor: 1
}
]]
</div>
`);
</div>`);
}
}

Expand Down Expand Up @@ -125,6 +123,49 @@ class CounterText extends tApp.Component {
}
}

class Text extends tApp.Component {
constructor(state, parent) {
super(state, parent);
if(this.state.text == null) {
this.state.text = "";
}
if(this.state.textInput1 == null) {
this.state.textInput1 = new TextInput({}, this);
}
if(this.state.textInput2 == null) {
this.state.textInput2 = new TextInput({}, this);
}
if(this.state.textDisplay == null) {
this.state.textDisplay = new TextDisplay({}, this);
}
}
render(props) {
if(this.state.text == "hello" || this.state.text == '"hello"') {
return `<div><p>HI! <span>hello there</span></p>${this.state.textDisplay}<p>HI! <span>hello there</span></p>${this.state.textInput1}<p>HI! <span>hello there</span></p>${this.state.textInput2}<p>HI! <span>hello there</span></p></div>`;
} else {
return `<div>${this.state.textDisplay}${this.state.textInput1}${this.state.textInput2}</div>`;
}
}
}

class TextInput extends tApp.Component {
constructor(state, parent) {
super(state, parent);
}
render(props) {
return `<input oninput="{{_this}}.parent.setState('text', this.value);" value="{{{ tApp.escape(parent.state.text) }}}" />`;
}
}

class TextDisplay extends tApp.Component {
constructor(state, parent) {
super(state, parent);
}
render(props) {
return `<p>{{{ tApp.escape(parent.state.text) }}}</p>`;
}
}

tApp.route("/", function(request) {
tApp.redirect("#/");
});
Expand Down Expand Up @@ -181,9 +222,11 @@ tApp.route("#/template", function(request) {
});

let counter = new CounterPreserved();
let textComponent = new Text();
tApp.route("#/components", function(request) {
tApp.renderTemplate("./views/components.html", {
counter: counter.toString()
counter: counter.toString(),
text: textComponent.toString()
});
});

Expand Down
6 changes: 5 additions & 1 deletion example/views/components.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@ <h3>Counter (Template-Based/Unpreserved):</h3>
[[ Counter {} ]]

<h3>Counter (Object-Based/Preserved):</h3>
{{ counter }}
{{ counter }}

<h3>Updating Text (Object-Based/Preserved):</h3>
<p>(Try typing "hello" and see tApp handle complex DOM changes)</p>
{{ text }}
146 changes: 141 additions & 5 deletions tApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class tApp {
static database;
static currentHash = "/";
static get version() {
return "v0.10.0";
return "v0.10.1";
}
static configure(params) {
if(params == null) {
Expand Down Expand Up @@ -362,6 +362,18 @@ class tApp {
});
});
}
static escape(string) {
let entityMap = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': '&quot;',
"'": '&#39;'
};
return string.replace(/[&<>"']/g, function (s) {
return entityMap[s];
});
}
static eval(code) {
return (function(code) {
return eval(code);
Expand Down Expand Up @@ -413,13 +425,130 @@ class tApp {
}
}
static updateComponent(component) {
let compiled = tApp.compileComponent(component, component.props, component.parent);
function htmlToDOM(html) {
if(html.includes("<body")) {
return new DOMParser().parseFromString(html, "text/html").childNodes[0];
} else {
return new DOMParser().parseFromString(html, "text/html").body.childNodes[0];
}
}
function compareChildren(before, after) {
if(before.childNodes.length != after.childNodes.length) {
return false;
}
for(let i = 0; i < before.childNodes.length; i++) {
if(before.childNodes[i].nodeName != after.childNodes[i].nodeName) {
return false;
}
if(before.childNodes[i].getAttribute("tapp-component") != after.childNodes[i].getAttribute("tApp-component")) {
return false;
}
}
return true;
}
function convertNode(before, after) {
if(after.attributes != null) {
for(let i = 0; i < after.attributes.length; i++) {
if(after.attributes.item(i).nodeName == "value") {
before.value = after.value;
} else {
before.setAttribute(after.attributes.item(i).nodeName, after.attributes.item(i).nodeValue);
}
}
}
if(before.nodeName == "#text" && after.nodeName == "#text") {
before.textContent = after.textContent;
}

if(after.childNodes.length == 0 || after.childNodes.length == 1 && after.childNodes[0].nodeName == "#text") {
before.innerHTML = after.innerHTML;
} else {
if(compareChildren(before, after)) {
for(let i = 0; i < after.childNodes.length; i++) {
convertNode(before.childNodes[i], after.childNodes[i])
}
} else {
let beforeChildren = [...before.childNodes];
let afterChildren = [...after.childNodes];
let beforeChildrenPersist = [...before.childNodes];
let afterChildrenPersist = [...after.childNodes];
let pointerBefore = 0;
let pointerAfter = 0;
while(pointerBefore < beforeChildren.length || pointerAfter < afterChildren.length) {
if(pointerBefore >= beforeChildren.length) {
beforeChildren.splice(pointerBefore, 0, null);
} else if(pointerAfter >= afterChildren.length) {
afterChildren.splice(pointerAfter, 0, null);
} else {
if(beforeChildren[pointerBefore].nodeName != afterChildren[pointerAfter].nodeName) {
if(beforeChildrenPersist.length > afterChildrenPersist.length) {
afterChildren.splice(pointerAfter, 0, null);
} else {
beforeChildren.splice(pointerBefore, 0, null);
}
}
}
pointerBefore++;
pointerAfter++;
}
//console.log("before", beforeChildren, beforeChildren.map(child => {if(child != null){ return child.data }else{ return "null"}}));
//console.log("after", afterChildren, afterChildren.map(child => {if(child != null){ return child.data }else{ return "null"}}));
for(let i = 0; i < beforeChildren.length; i++) {
let nullBefore = beforeChildren.length == beforeChildren.filter(el => el == null || el.nodeName == "#text").length;
if(beforeChildren[i] == null && afterChildren[i] == null) {
} else if(beforeChildren[i] == null) {
if(nullBefore) {
before.appendChild(afterChildren[i]);
} else {
let nextNotNull;
for(let j = i; nextNotNull == null && j < beforeChildren.length; j++) {
if(beforeChildren[j] != null) {
nextNotNull = beforeChildren[j];
}
}
if(nextNotNull == null) {
let prevNotNull;
for(let j = i; prevNotNull == null && j < beforeChildren.length; j--) {
if(beforeChildren[j] != null) {
prevNotNull = beforeChildren[j];
}
}
prevNotNull.insertAdjacentElement("afterend", afterChildren[i]);
} else {
nextNotNull.insertAdjacentElement("beforebegin", afterChildren[i]);
}
}
} else if(afterChildren[i] == null) {
beforeChildren[i].remove();
beforeChildren[i] = null;
} else {
convertNode(beforeChildren[i], afterChildren[i]);
}
}
}
}
}
let compiled = htmlToDOM(tApp.compileComponent(component, component.props, component.parent));
let els = document.querySelectorAll(`[tapp-component="${component.id}"]`);
for(let i = 0; i < els.length; i++) {
els[i].outerHTML = compiled;
convertNode(els[i], compiled);
}
}
static compileComponent(component, props = {}, parent = "global") {
function htmlToDOM(html) {
if(html.includes("<body")) {
return new DOMParser().parseFromString(html, "text/html").childNodes[0];
} else {
return new DOMParser().parseFromString(html, "text/html").body.childNodes[0];
}
}
function htmlToDOMCount(html) {
if(html.includes("<body")) {
return new DOMParser().parseFromString(html, "text/html").childNodes.length;
} else {
return new DOMParser().parseFromString(html, "text/html").body.childNodes.length;
}
}
if(component instanceof tApp.Component) {
tApp.components[component.id] = component;
if(typeof props == "string") {
Expand All @@ -430,7 +559,14 @@ class tApp {
if(component.parent != null) {
parentState = component.parent.state;
}
return tApp.compileTemplate(rendered.replace(`>`, ` tapp-component="${component.id}">`), {
let count = htmlToDOMCount(rendered);
if(count != 1) {
throw "tAppComponentError: Component render output must contain exactly one node/element but can contain subnodes/subelements. To resolve this issue, wrap the entire output of the render in a div or another grouping element. If you only have one node/element, unintentional whitespace at the beginning or end of the render output could be the source of the issue since whitespace can be interpreted as a text node/element.";
}
let domRendered = htmlToDOM(rendered);
domRendered.setAttribute("tapp-component", component.id);
rendered = domRendered.outerHTML;
return tApp.compileTemplate(rendered, {
props: props,
state: component.state,
parent: {
Expand Down Expand Up @@ -939,7 +1075,7 @@ tApp.GlobalComponent = (function() {
super(state, "");
}
render(props) {
return "";
return "<div></div>";
}
get id() {
return "global";
Expand Down

0 comments on commit e2618a7

Please sign in to comment.