Skip to content

Commit e2618a7

Browse files
committed
DOM diffing
1 parent 19a3e1d commit e2618a7

File tree

3 files changed

+194
-11
lines changed

3 files changed

+194
-11
lines changed

example/config.js

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,7 @@ class Counter extends tApp.Component {
4646
for(let i = 0; i < this.children.length; i++) {
4747
this.children[i].destroy();
4848
}
49-
return (`
50-
<div>
49+
return (`<div>
5150
[[
5251
CounterButton
5352
{
@@ -66,8 +65,7 @@ class Counter extends tApp.Component {
6665
incrementor: 1
6766
}
6867
]]
69-
</div>
70-
`);
68+
</div>`);
7169
}
7270
}
7371

@@ -125,6 +123,49 @@ class CounterText extends tApp.Component {
125123
}
126124
}
127125

126+
class Text extends tApp.Component {
127+
constructor(state, parent) {
128+
super(state, parent);
129+
if(this.state.text == null) {
130+
this.state.text = "";
131+
}
132+
if(this.state.textInput1 == null) {
133+
this.state.textInput1 = new TextInput({}, this);
134+
}
135+
if(this.state.textInput2 == null) {
136+
this.state.textInput2 = new TextInput({}, this);
137+
}
138+
if(this.state.textDisplay == null) {
139+
this.state.textDisplay = new TextDisplay({}, this);
140+
}
141+
}
142+
render(props) {
143+
if(this.state.text == "hello" || this.state.text == '"hello"') {
144+
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>`;
145+
} else {
146+
return `<div>${this.state.textDisplay}${this.state.textInput1}${this.state.textInput2}</div>`;
147+
}
148+
}
149+
}
150+
151+
class TextInput extends tApp.Component {
152+
constructor(state, parent) {
153+
super(state, parent);
154+
}
155+
render(props) {
156+
return `<input oninput="{{_this}}.parent.setState('text', this.value);" value="{{{ tApp.escape(parent.state.text) }}}" />`;
157+
}
158+
}
159+
160+
class TextDisplay extends tApp.Component {
161+
constructor(state, parent) {
162+
super(state, parent);
163+
}
164+
render(props) {
165+
return `<p>{{{ tApp.escape(parent.state.text) }}}</p>`;
166+
}
167+
}
168+
128169
tApp.route("/", function(request) {
129170
tApp.redirect("#/");
130171
});
@@ -181,9 +222,11 @@ tApp.route("#/template", function(request) {
181222
});
182223

183224
let counter = new CounterPreserved();
225+
let textComponent = new Text();
184226
tApp.route("#/components", function(request) {
185227
tApp.renderTemplate("./views/components.html", {
186-
counter: counter.toString()
228+
counter: counter.toString(),
229+
text: textComponent.toString()
187230
});
188231
});
189232

example/views/components.html

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,8 @@ <h3>Counter (Template-Based/Unpreserved):</h3>
22
[[ Counter {} ]]
33

44
<h3>Counter (Object-Based/Preserved):</h3>
5-
{{ counter }}
5+
{{ counter }}
6+
7+
<h3>Updating Text (Object-Based/Preserved):</h3>
8+
<p>(Try typing "hello" and see tApp handle complex DOM changes)</p>
9+
{{ text }}

tApp.js

Lines changed: 141 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class tApp {
88
static database;
99
static currentHash = "/";
1010
static get version() {
11-
return "v0.10.0";
11+
return "v0.10.1";
1212
}
1313
static configure(params) {
1414
if(params == null) {
@@ -362,6 +362,18 @@ class tApp {
362362
});
363363
});
364364
}
365+
static escape(string) {
366+
let entityMap = {
367+
"&": "&amp;",
368+
"<": "&lt;",
369+
">": "&gt;",
370+
'"': '&quot;',
371+
"'": '&#39;'
372+
};
373+
return string.replace(/[&<>"']/g, function (s) {
374+
return entityMap[s];
375+
});
376+
}
365377
static eval(code) {
366378
return (function(code) {
367379
return eval(code);
@@ -413,13 +425,130 @@ class tApp {
413425
}
414426
}
415427
static updateComponent(component) {
416-
let compiled = tApp.compileComponent(component, component.props, component.parent);
428+
function htmlToDOM(html) {
429+
if(html.includes("<body")) {
430+
return new DOMParser().parseFromString(html, "text/html").childNodes[0];
431+
} else {
432+
return new DOMParser().parseFromString(html, "text/html").body.childNodes[0];
433+
}
434+
}
435+
function compareChildren(before, after) {
436+
if(before.childNodes.length != after.childNodes.length) {
437+
return false;
438+
}
439+
for(let i = 0; i < before.childNodes.length; i++) {
440+
if(before.childNodes[i].nodeName != after.childNodes[i].nodeName) {
441+
return false;
442+
}
443+
if(before.childNodes[i].getAttribute("tapp-component") != after.childNodes[i].getAttribute("tApp-component")) {
444+
return false;
445+
}
446+
}
447+
return true;
448+
}
449+
function convertNode(before, after) {
450+
if(after.attributes != null) {
451+
for(let i = 0; i < after.attributes.length; i++) {
452+
if(after.attributes.item(i).nodeName == "value") {
453+
before.value = after.value;
454+
} else {
455+
before.setAttribute(after.attributes.item(i).nodeName, after.attributes.item(i).nodeValue);
456+
}
457+
}
458+
}
459+
if(before.nodeName == "#text" && after.nodeName == "#text") {
460+
before.textContent = after.textContent;
461+
}
462+
463+
if(after.childNodes.length == 0 || after.childNodes.length == 1 && after.childNodes[0].nodeName == "#text") {
464+
before.innerHTML = after.innerHTML;
465+
} else {
466+
if(compareChildren(before, after)) {
467+
for(let i = 0; i < after.childNodes.length; i++) {
468+
convertNode(before.childNodes[i], after.childNodes[i])
469+
}
470+
} else {
471+
let beforeChildren = [...before.childNodes];
472+
let afterChildren = [...after.childNodes];
473+
let beforeChildrenPersist = [...before.childNodes];
474+
let afterChildrenPersist = [...after.childNodes];
475+
let pointerBefore = 0;
476+
let pointerAfter = 0;
477+
while(pointerBefore < beforeChildren.length || pointerAfter < afterChildren.length) {
478+
if(pointerBefore >= beforeChildren.length) {
479+
beforeChildren.splice(pointerBefore, 0, null);
480+
} else if(pointerAfter >= afterChildren.length) {
481+
afterChildren.splice(pointerAfter, 0, null);
482+
} else {
483+
if(beforeChildren[pointerBefore].nodeName != afterChildren[pointerAfter].nodeName) {
484+
if(beforeChildrenPersist.length > afterChildrenPersist.length) {
485+
afterChildren.splice(pointerAfter, 0, null);
486+
} else {
487+
beforeChildren.splice(pointerBefore, 0, null);
488+
}
489+
}
490+
}
491+
pointerBefore++;
492+
pointerAfter++;
493+
}
494+
//console.log("before", beforeChildren, beforeChildren.map(child => {if(child != null){ return child.data }else{ return "null"}}));
495+
//console.log("after", afterChildren, afterChildren.map(child => {if(child != null){ return child.data }else{ return "null"}}));
496+
for(let i = 0; i < beforeChildren.length; i++) {
497+
let nullBefore = beforeChildren.length == beforeChildren.filter(el => el == null || el.nodeName == "#text").length;
498+
if(beforeChildren[i] == null && afterChildren[i] == null) {
499+
} else if(beforeChildren[i] == null) {
500+
if(nullBefore) {
501+
before.appendChild(afterChildren[i]);
502+
} else {
503+
let nextNotNull;
504+
for(let j = i; nextNotNull == null && j < beforeChildren.length; j++) {
505+
if(beforeChildren[j] != null) {
506+
nextNotNull = beforeChildren[j];
507+
}
508+
}
509+
if(nextNotNull == null) {
510+
let prevNotNull;
511+
for(let j = i; prevNotNull == null && j < beforeChildren.length; j--) {
512+
if(beforeChildren[j] != null) {
513+
prevNotNull = beforeChildren[j];
514+
}
515+
}
516+
prevNotNull.insertAdjacentElement("afterend", afterChildren[i]);
517+
} else {
518+
nextNotNull.insertAdjacentElement("beforebegin", afterChildren[i]);
519+
}
520+
}
521+
} else if(afterChildren[i] == null) {
522+
beforeChildren[i].remove();
523+
beforeChildren[i] = null;
524+
} else {
525+
convertNode(beforeChildren[i], afterChildren[i]);
526+
}
527+
}
528+
}
529+
}
530+
}
531+
let compiled = htmlToDOM(tApp.compileComponent(component, component.props, component.parent));
417532
let els = document.querySelectorAll(`[tapp-component="${component.id}"]`);
418533
for(let i = 0; i < els.length; i++) {
419-
els[i].outerHTML = compiled;
534+
convertNode(els[i], compiled);
420535
}
421536
}
422537
static compileComponent(component, props = {}, parent = "global") {
538+
function htmlToDOM(html) {
539+
if(html.includes("<body")) {
540+
return new DOMParser().parseFromString(html, "text/html").childNodes[0];
541+
} else {
542+
return new DOMParser().parseFromString(html, "text/html").body.childNodes[0];
543+
}
544+
}
545+
function htmlToDOMCount(html) {
546+
if(html.includes("<body")) {
547+
return new DOMParser().parseFromString(html, "text/html").childNodes.length;
548+
} else {
549+
return new DOMParser().parseFromString(html, "text/html").body.childNodes.length;
550+
}
551+
}
423552
if(component instanceof tApp.Component) {
424553
tApp.components[component.id] = component;
425554
if(typeof props == "string") {
@@ -430,7 +559,14 @@ class tApp {
430559
if(component.parent != null) {
431560
parentState = component.parent.state;
432561
}
433-
return tApp.compileTemplate(rendered.replace(`>`, ` tapp-component="${component.id}">`), {
562+
let count = htmlToDOMCount(rendered);
563+
if(count != 1) {
564+
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.";
565+
}
566+
let domRendered = htmlToDOM(rendered);
567+
domRendered.setAttribute("tapp-component", component.id);
568+
rendered = domRendered.outerHTML;
569+
return tApp.compileTemplate(rendered, {
434570
props: props,
435571
state: component.state,
436572
parent: {
@@ -939,7 +1075,7 @@ tApp.GlobalComponent = (function() {
9391075
super(state, "");
9401076
}
9411077
render(props) {
942-
return "";
1078+
return "<div></div>";
9431079
}
9441080
get id() {
9451081
return "global";

0 commit comments

Comments
 (0)