Working diff update algorithm

This commit is contained in:
2020-05-16 15:44:43 +02:00
parent 419c32a198
commit 59d812613e
6 changed files with 297 additions and 71 deletions

View File

@@ -1,17 +1,14 @@
plugins { plugins {
kotlin("multiplatform") version "1.4-M2-eap-68" kotlin("multiplatform") version "1.3.71"
`maven-publish` `maven-publish`
} }
group = "nl.astraeus" group = "nl.astraeus"
version = "0.1.20-SNAPSHOT" version = "0.1.21-SNAPSHOT"
repositories { repositories {
maven { setUrl("https://dl.bintray.com/kotlin/kotlin-eap") }
mavenCentral() mavenCentral()
maven { jcenter()
url = uri("https://dl.bintray.com/kotlin/kotlin-dev")
}
} }
kotlin { kotlin {
@@ -21,6 +18,11 @@ kotlin {
js { js {
browser { browser {
//produceKotlinLibrary() //produceKotlinLibrary()
testTask {
useKarma {
useChromeHeadless()
}
}
} }
} }
@@ -36,7 +38,12 @@ kotlin {
dependencies { dependencies {
implementation(kotlin("stdlib-js")) implementation(kotlin("stdlib-js"))
api("org.jetbrains.kotlinx:kotlinx-html-js:0.7.2-build-1716") api("org.jetbrains.kotlinx:kotlinx-html-js:0.7.1")
}
}
val jsTest by getting {
dependencies {
implementation(kotlin("test-js"))
} }
} }
} }

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id="komp" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="nl.astraeus" external.system.module.version="0.1.20-SNAPSHOT" type="JAVA_MODULE" version="4"> <module external.linked.project.id="komp" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="nl.astraeus" external.system.module.version="0.1.21-SNAPSHOT" type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true"> <component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output /> <exclude-output />
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">

View File

@@ -1,12 +1,14 @@
package nl.astraeus.komp package nl.astraeus.komp
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.Node import org.w3c.dom.Node
import org.w3c.dom.NodeList import org.w3c.dom.NodeList
import org.w3c.dom.events.Event import org.w3c.dom.events.Event
import org.w3c.dom.get import org.w3c.dom.get
const val HASH_VALUE = "komp-hash-value" const val HASH_VALUE = "komp-hash-value"
//const val HASH_ATTRIBUTE = "data-komp-hash" //const val HASH_ATTRIBUTE = "data-komp-hash"
const val EVENT_ATTRIBUTE = "data-komp-events" const val EVENT_ATTRIBUTE = "data-komp-events"
@@ -59,14 +61,17 @@ object DiffPatch {
if (oldNode is HTMLElement && newNode is HTMLElement) { if (oldNode is HTMLElement && newNode is HTMLElement) {
if (oldNode.nodeName == newNode.nodeName) { if (oldNode.nodeName == newNode.nodeName) {
if (Komponent.logReplaceEvent) { if (Komponent.logReplaceEvent) {
console.log("Update attributes", oldNode.innerHTML, newNode.innerHTML) console.log("Update attributes", oldNode.nodeName, newNode.nodeName)
} }
updateAttributes(oldNode, newNode); updateAttributes(oldNode, newNode);
if (Komponent.logReplaceEvent) { if (Komponent.logReplaceEvent) {
console.log("Update children", oldNode.innerHTML, newNode.innerHTML) console.log("Update events", oldNode.nodeName, newNode.nodeName)
}
updateEvents(oldNode, newNode)
if (Komponent.logReplaceEvent) {
console.log("Update children", oldNode.nodeName, newNode.nodeName)
} }
updateChildren(oldNode, newNode) updateChildren(oldNode, newNode)
updateEvents(oldNode, newNode)
oldNode.setKompHash(newNode.getKompHash()) oldNode.setKompHash(newNode.getKompHash())
return oldNode return oldNode
} }
@@ -75,20 +80,40 @@ object DiffPatch {
if (Komponent.logReplaceEvent) { if (Komponent.logReplaceEvent) {
console.log("Replace node (type)", oldNode.nodeType, oldNode, newNode) console.log("Replace node (type)", oldNode.nodeType, oldNode, newNode)
} }
replaceNode(oldNode, newNode)
oldNode.parentNode?.replaceChild(newNode, oldNode)
//replaceNode(oldNode, newNode)
return newNode return newNode
} }
private fun updateAttributes(oldNode: HTMLElement, newNode: HTMLElement) { private fun updateAttributes(oldNode: HTMLElement, newNode: HTMLElement) {
// removed attributes // removed attributes
for (index in 0 until oldNode.attributes.length) { for (name in oldNode.getAttributeNames()) {
val attr = oldNode.attributes[index] val attr = oldNode.attributes[name]
if (attr != null && newNode.attributes[attr.name] == null) { if (attr != null && newNode.getAttribute(name) == null) {
oldNode.removeAttribute(attr.name) oldNode.removeAttribute(name)
} }
} }
for (name in newNode.getAttributeNames()) {
val value = newNode.getAttribute(name)
val oldValue = oldNode.getAttribute(name)
if (value != oldValue) {
if (value != null) {
oldNode.setAttribute(name, value)
}else {
oldNode.removeAttribute(name)
}
}
}
if (newNode is HTMLInputElement && oldNode is HTMLInputElement) {
oldNode.value = newNode.value
}
/*
for (index in 0 until newNode.attributes.length) { for (index in 0 until newNode.attributes.length) {
val attr = newNode.attributes[index] val attr = newNode.attributes[index]
@@ -100,6 +125,7 @@ object DiffPatch {
} }
} }
} }
*/
} }
private fun updateChildren(oldNode: HTMLElement, newNode: HTMLElement) { private fun updateChildren(oldNode: HTMLElement, newNode: HTMLElement) {
@@ -107,7 +133,14 @@ object DiffPatch {
var newIndex = 0 var newIndex = 0
if (Komponent.logReplaceEvent) { if (Komponent.logReplaceEvent) {
console.log("updateChildren HTML old/new", oldNode.innerHTML, newNode.innerHTML) console.log(
"updateChildren HTML old(${oldNode.childNodes.length})",
oldNode.innerHTML
)
console.log(
"updateChildren HTML new(${newNode.childNodes.length})",
newNode.innerHTML
)
} }
while (newIndex < newNode.childNodes.length) { while (newIndex < newNode.childNodes.length) {
@@ -120,11 +153,11 @@ object DiffPatch {
val oldChildNode = oldNode.childNodes[oldIndex] val oldChildNode = oldNode.childNodes[oldIndex]
if (oldChildNode != null && newChildNode != null) { if (oldChildNode != null && newChildNode != null) {
/* /*
if (Komponent.logReplaceEvent) { if (Komponent.logReplaceEvent) {
console.log(">>> updateChildren old/new", oldChildNode, newChildNode) console.log(">>> updateChildren old/new", oldChildNode, newChildNode)
} }
*/ */
if (Komponent.logReplaceEvent) { if (Komponent.logReplaceEvent) {
console.log("Update node Old/new", oldChildNode, newChildNode) console.log("Update node Old/new", oldChildNode, newChildNode)
@@ -138,7 +171,7 @@ object DiffPatch {
val oldHash = oldChildNode.getKompHash() val oldHash = oldChildNode.getKompHash()
val newHash = newChildNode.getKompHash() val newHash = newChildNode.getKompHash()
if (newHash != null) { if (newHash >= 0) {
val oldNodeWithNewHashIndex = oldNode.childNodes.findNodeHashIndex(newHash) val oldNodeWithNewHashIndex = oldNode.childNodes.findNodeHashIndex(newHash)
if (Komponent.logReplaceEvent) { if (Komponent.logReplaceEvent) {
@@ -146,7 +179,7 @@ object DiffPatch {
} }
if (oldNodeWithNewHashIndex > oldIndex) { if (oldNodeWithNewHashIndex > oldIndex) {
if (oldHash != null) { if (oldHash >= 0) {
val newNodeWithOldHashIndex = newNode.childNodes.findNodeHashIndex(oldHash) val newNodeWithOldHashIndex = newNode.childNodes.findNodeHashIndex(oldHash)
// remove i.o. swap // remove i.o. swap
@@ -177,7 +210,7 @@ object DiffPatch {
oldIndex++ oldIndex++
continue continue
} }
} else if (oldHash != null && newNode.childNodes.findNodeHashIndex(oldHash) > newIndex) { } else if (oldHash >= 0 && newNode.childNodes.findNodeHashIndex(oldHash) > newIndex) {
if (Komponent.logReplaceEvent) { if (Komponent.logReplaceEvent) {
console.log("newNodeWithOldHashIndex", oldHash, newNode.childNodes.findNodeHashIndex(oldHash)) console.log("newNodeWithOldHashIndex", oldHash, newNode.childNodes.findNodeHashIndex(oldHash))
} }
@@ -189,27 +222,36 @@ object DiffPatch {
} }
} }
updateNode(oldChildNode, newChildNode) val updatedNode = updateNode(oldChildNode, newChildNode)
if (updatedNode == newChildNode) {
if (oldChildNode is HTMLElement && newChildNode is HTMLElement) {
updateEvents(oldChildNode, newChildNode)
}
oldIndex++
continue
}
} else { } else {
if (Komponent.logReplaceEvent) { if (Komponent.logReplaceEvent) {
console.log("Null node", oldChildNode, newChildNode) console.log("Null node", oldChildNode, newChildNode)
} }
} }
oldIndex++
newIndex++
} else { } else {
if (Komponent.logReplaceEvent) { if (Komponent.logReplaceEvent) {
console.log("Append Old/new/node", oldIndex, newIndex, newChildNode) console.log("Append Old/new/node", oldIndex, newIndex, newChildNode)
} }
oldNode.append(newChildNode) oldNode.append(newChildNode)
oldIndex++
} }
/* /*
if (Komponent.logReplaceEvent) { if (Komponent.logReplaceEvent) {
console.log("<<< Updated Old/new", oldNode.innerHTML, newNode.innerHTML) console.log("<<< Updated Old/new", oldNode.innerHTML, newNode.innerHTML)
} }
*/ */
oldIndex++
newIndex++
} }
while (oldIndex < oldNode.childNodes.length) { while (oldIndex < oldNode.childNodes.length) {
@@ -229,16 +271,25 @@ object DiffPatch {
val newEvents = (newNode.getAttribute(EVENT_ATTRIBUTE) ?: "").split(",") val newEvents = (newNode.getAttribute(EVENT_ATTRIBUTE) ?: "").split(",")
if (Komponent.logReplaceEvent) {
console.log("Update events", oldNode.getAttribute(EVENT_ATTRIBUTE), newNode.getAttribute(EVENT_ATTRIBUTE))
}
for (event in newEvents) { for (event in newEvents) {
if (event.isNotBlank()) { if (event.isNotBlank()) {
val oldNodeEvent = oldNode.asDynamic()["event-$event"] val oldNodeEvent = oldNode.asDynamic()["event-$event"]
val newNodeEvent = newNode.asDynamic()["event-$event"] val newNodeEvent = newNode.asDynamic()["event-$event"]
if (oldNodeEvent != null) { if (oldNodeEvent != null) {
if (Komponent.logReplaceEvent) {
console.log("Remove old event $event")
}
oldNode.removeEventListener(event, oldNodeEvent as ((Event) -> Unit), null) oldNode.removeEventListener(event, oldNodeEvent as ((Event) -> Unit), null)
} }
if (newNodeEvent != null) { if (newNodeEvent != null) {
oldNode.addEventListener(event, newNodeEvent as ((Event) -> Unit), null) if (Komponent.logReplaceEvent) {
oldNode.asDynamic()["event-$event"] = newNodeEvent console.log("Set event $event on", oldNode)
}
oldNode.setEvent(event, newNodeEvent as ((Event) -> Unit))
} }
oldEvents.remove(event) oldEvents.remove(event)
} }
@@ -261,17 +312,41 @@ object DiffPatch {
private fun replaceNode(oldNode: Node, newNode: Node) { private fun replaceNode(oldNode: Node, newNode: Node) {
oldNode.parentNode?.also { parent -> oldNode.parentNode?.also { parent ->
val clone = newNode.cloneNode(true) val clone = newNode.cloneNode(true)
if (newNode is HTMLElement) { cloneEvents(clone, newNode)
val events = (newNode.getAttribute(EVENT_ATTRIBUTE) ?: "").split(",")
for (event in events) {
val foundEvent = newNode.asDynamic()["event-$event"]
if (foundEvent != null) {
clone.addEventListener(event, foundEvent as ((Event) -> Unit), null)
}
}
}
parent.replaceChild(clone, oldNode) parent.replaceChild(clone, oldNode)
} }
} }
private fun cloneEvents(destination: Node, source: Node) {
if (source is HTMLElement && destination is HTMLElement) {
val events = (source.getAttribute(EVENT_ATTRIBUTE) ?: "").split(",")
for (event in events) {
if (event.isNotBlank()) {
if (Komponent.logReplaceEvent) {
console.log("Clone event $event on", source)
}
val foundEvent = source.asDynamic()["event-$event"]
if (foundEvent != null) {
if (Komponent.logReplaceEvent) {
console.log("Clone add eventlistener", foundEvent)
}
destination.setEvent(event, foundEvent as ((Event) -> Unit))
} else {
if (Komponent.logReplaceEvent) {
console.log("Event not found $event", source)
}
}
}
}
}
for (index in 0 until source.childNodes.length) {
destination.childNodes[index]?.also { destinationChild ->
source.childNodes[index]?.also { sourceChild ->
cloneEvents(destinationChild, sourceChild)
}
}
}
}
} }

View File

@@ -7,7 +7,7 @@ import org.w3c.dom.events.Event
import kotlin.browser.document import kotlin.browser.document
@Suppress("NOTHING_TO_INLINE") @Suppress("NOTHING_TO_INLINE")
private inline fun HTMLElement.setEvent(name: String, noinline callback: (Event) -> Unit): Unit { inline fun HTMLElement.setEvent(name: String, noinline callback: (Event) -> Unit): Unit {
val eventName = if (name.startsWith("on")) { val eventName = if (name.startsWith("on")) {
name.substring(2) name.substring(2)
} else { } else {
@@ -103,19 +103,19 @@ class HtmlBuilder(
} }
} }
tag.attributesEntries.forEach { for ((key, value) in tag.attributesEntries) {
if (Komponent.updateStrategy == UpdateStrategy.DOM_DIFF) { if (key == "class") {
val key_value = "${it.key}-${it.value}" val classes = value.split(Regex("\\s+"))
hash = hash * 37 + key_value.hashCode()
}
if (it.key == "class") {
val classes = it.value.split(Regex("\\s+"))
val classNames = StringBuilder() val classNames = StringBuilder()
for (cls in classes) { for (cls in classes) {
val cssStyle = komponent.declaredStyles[cls] val cssStyle = komponent.declaredStyles[cls]
if (cssStyle != null) { if (cssStyle != null) {
if (Komponent.updateStrategy == UpdateStrategy.DOM_DIFF) {
hash = hash * 37 + cssStyle.hashCode()
}
if (cls.endsWith(":hover")) { if (cls.endsWith(":hover")) {
val oldOnMouseOver = element.onmouseover val oldOnMouseOver = element.onmouseover
val oldOnMouseOut = element.onmouseout val oldOnMouseOut = element.onmouseout
@@ -138,15 +138,31 @@ class HtmlBuilder(
element.setStyles(cssStyle) element.setStyles(cssStyle)
} }
} else { } else {
if (Komponent.updateStrategy == UpdateStrategy.DOM_DIFF) {
hash = hash * 37 + cls.hashCode()
}
classNames.append(cls) classNames.append(cls)
classNames.append(" ") classNames.append(" ")
} }
} }
element.className = classNames.toString() element.className = classNames.toString()
} else {
element.setAttribute(it.key, it.value) if (Komponent.updateStrategy == UpdateStrategy.DOM_DIFF) {
val key_value = "${key}-${classNames}"
hash = hash * 37 + key_value.hashCode()
} }
} else {
element.setAttribute(key, value)
if (Komponent.updateStrategy == UpdateStrategy.DOM_DIFF) {
val key_value = "${key}-${value}"
hash = hash * 37 + key_value.hashCode()
}
}
} }
if (Komponent.updateStrategy == UpdateStrategy.DOM_DIFF) { if (Komponent.updateStrategy == UpdateStrategy.DOM_DIFF) {

View File

@@ -1,6 +1,5 @@
package nl.astraeus.komp package nl.astraeus.komp
import kotlinx.html.Tag
import kotlinx.html.div import kotlinx.html.div
import org.w3c.dom.HTMLDivElement import org.w3c.dom.HTMLDivElement
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
@@ -8,20 +7,21 @@ import org.w3c.dom.Node
import org.w3c.dom.css.CSSStyleDeclaration import org.w3c.dom.css.CSSStyleDeclaration
import kotlin.browser.document import kotlin.browser.document
public typealias CssStyle = CSSStyleDeclaration.() -> Unit typealias CssStyle = CSSStyleDeclaration.() -> Unit
fun Tag.include(component: Komponent) { fun HtmlConsumer.include(component: Komponent) {
if (Komponent.updateStrategy == UpdateStrategy.REPLACE) {
if (component.element != null) { if (component.element != null) {
component.update() component.update()
} else { } else {
component.refresh() component.refresh()
} }
val consumer = this.consumer component.element?.also {
val element = component.element append(it)
}
if (consumer is HtmlBuilder && element != null) { } else {
consumer.append(element) append(component.create())
} }
} }
@@ -47,10 +47,6 @@ abstract class Komponent {
consumer.render() consumer.render()
val result = consumer.finalize() val result = consumer.finalize()
if (logReplaceEvent) {
console.log("Element hash", result, result.getKompHash())
}
element = result element = result
return result return result

View File

@@ -0,0 +1,132 @@
package nl.astraeus.komp
import kotlinx.html.*
import kotlinx.html.js.onClickFunction
import org.w3c.dom.HTMLElement
import org.w3c.dom.Node
import org.w3c.dom.get
import kotlin.test.Test
import kotlin.test.assertTrue
fun nodesEqual(node1: Node, node2: Node): Boolean {
if (node1.childNodes.length != node1.childNodes.length) {
return false
}
if (node1 is HTMLElement && node2 is HTMLElement) {
if (node1.attributes.length != node2.attributes.length) {
return false
}
for (index in 0 until node1.attributes.length) {
node1.attributes[index]?.also { attr1 ->
val attr2 = node2.getAttribute(attr1.name)
if (attr1.value != attr2) {
return false
}
}
}
for (index in 0 until node1.childNodes.length) {
node1.childNodes[index]?.also { child1 ->
node2.childNodes[index]?.also { child2 ->
if (!nodesEqual(child1, child2)) {
return false
}
}
}
}
}
return true
}
class TestUpdate {
@Test
fun testCompare1() {
val dom1 = HtmlBuilder.create {
div {
div(classes = "bla") {
span {
+" Some Text "
}
table {
tr {
td {
+"Table column"
}
}
}
}
}
}
val dom2 = HtmlBuilder.create {
div {
span {
id = "123"
+"New dom!"
}
input {
value = "bla"
}
}
}
DiffPatch.updateNode(dom1, dom2)
assertTrue(nodesEqual(dom1, dom2), "Updated dom not equal to original")
}
@Test
fun testCompare2() {
val dom1 = HtmlBuilder.create {
div {
div(classes = "bla") {
span {
+" Some Text "
}
table {
tr {
th {
+ "Header"
}
}
tr {
td {
+"Table column"
}
}
}
}
}
}
val dom2 = HtmlBuilder.create {
div {
div {
span {
+ "Other text"
}
}
span {
id = "123"
+"New dom!"
}
input {
value = "bla"
onClickFunction = {
println("Clickerdyclick!")
}
}
}
}
Komponent.logReplaceEvent = true
DiffPatch.updateNode(dom1, dom2)
assertTrue(nodesEqual(dom1, dom2), "Updated dom not equal to original")
}
}