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 {
kotlin("multiplatform") version "1.4-M2-eap-68"
kotlin("multiplatform") version "1.3.71"
`maven-publish`
}
group = "nl.astraeus"
version = "0.1.20-SNAPSHOT"
version = "0.1.21-SNAPSHOT"
repositories {
maven { setUrl("https://dl.bintray.com/kotlin/kotlin-eap") }
mavenCentral()
maven {
url = uri("https://dl.bintray.com/kotlin/kotlin-dev")
}
jcenter()
}
kotlin {
@@ -21,6 +18,11 @@ kotlin {
js {
browser {
//produceKotlinLibrary()
testTask {
useKarma {
useChromeHeadless()
}
}
}
}
@@ -36,7 +38,12 @@ kotlin {
dependencies {
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"?>
<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">
<exclude-output />
<content url="file://$MODULE_DIR$">

View File

@@ -1,12 +1,14 @@
package nl.astraeus.komp
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.Node
import org.w3c.dom.NodeList
import org.w3c.dom.events.Event
import org.w3c.dom.get
const val HASH_VALUE = "komp-hash-value"
//const val HASH_ATTRIBUTE = "data-komp-hash"
const val EVENT_ATTRIBUTE = "data-komp-events"
@@ -59,14 +61,17 @@ object DiffPatch {
if (oldNode is HTMLElement && newNode is HTMLElement) {
if (oldNode.nodeName == newNode.nodeName) {
if (Komponent.logReplaceEvent) {
console.log("Update attributes", oldNode.innerHTML, newNode.innerHTML)
console.log("Update attributes", oldNode.nodeName, newNode.nodeName)
}
updateAttributes(oldNode, newNode);
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)
updateEvents(oldNode, newNode)
oldNode.setKompHash(newNode.getKompHash())
return oldNode
}
@@ -75,20 +80,40 @@ object DiffPatch {
if (Komponent.logReplaceEvent) {
console.log("Replace node (type)", oldNode.nodeType, oldNode, newNode)
}
replaceNode(oldNode, newNode)
oldNode.parentNode?.replaceChild(newNode, oldNode)
//replaceNode(oldNode, newNode)
return newNode
}
private fun updateAttributes(oldNode: HTMLElement, newNode: HTMLElement) {
// removed attributes
for (index in 0 until oldNode.attributes.length) {
val attr = oldNode.attributes[index]
for (name in oldNode.getAttributeNames()) {
val attr = oldNode.attributes[name]
if (attr != null && newNode.attributes[attr.name] == null) {
oldNode.removeAttribute(attr.name)
if (attr != null && newNode.getAttribute(name) == null) {
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) {
val attr = newNode.attributes[index]
@@ -100,6 +125,7 @@ object DiffPatch {
}
}
}
*/
}
private fun updateChildren(oldNode: HTMLElement, newNode: HTMLElement) {
@@ -107,7 +133,14 @@ object DiffPatch {
var newIndex = 0
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) {
@@ -138,7 +171,7 @@ object DiffPatch {
val oldHash = oldChildNode.getKompHash()
val newHash = newChildNode.getKompHash()
if (newHash != null) {
if (newHash >= 0) {
val oldNodeWithNewHashIndex = oldNode.childNodes.findNodeHashIndex(newHash)
if (Komponent.logReplaceEvent) {
@@ -146,7 +179,7 @@ object DiffPatch {
}
if (oldNodeWithNewHashIndex > oldIndex) {
if (oldHash != null) {
if (oldHash >= 0) {
val newNodeWithOldHashIndex = newNode.childNodes.findNodeHashIndex(oldHash)
// remove i.o. swap
@@ -177,7 +210,7 @@ object DiffPatch {
oldIndex++
continue
}
} else if (oldHash != null && newNode.childNodes.findNodeHashIndex(oldHash) > newIndex) {
} else if (oldHash >= 0 && newNode.childNodes.findNodeHashIndex(oldHash) > newIndex) {
if (Komponent.logReplaceEvent) {
console.log("newNodeWithOldHashIndex", oldHash, newNode.childNodes.findNodeHashIndex(oldHash))
}
@@ -189,17 +222,29 @@ 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 {
if (Komponent.logReplaceEvent) {
console.log("Null node", oldChildNode, newChildNode)
}
}
oldIndex++
newIndex++
} else {
if (Komponent.logReplaceEvent) {
console.log("Append Old/new/node", oldIndex, newIndex, newChildNode)
}
oldNode.append(newChildNode)
oldIndex++
}
/*
@@ -207,9 +252,6 @@ object DiffPatch {
console.log("<<< Updated Old/new", oldNode.innerHTML, newNode.innerHTML)
}
*/
oldIndex++
newIndex++
}
while (oldIndex < oldNode.childNodes.length) {
@@ -229,16 +271,25 @@ object DiffPatch {
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) {
if (event.isNotBlank()) {
val oldNodeEvent = oldNode.asDynamic()["event-$event"]
val newNodeEvent = newNode.asDynamic()["event-$event"]
if (oldNodeEvent != null) {
if (Komponent.logReplaceEvent) {
console.log("Remove old event $event")
}
oldNode.removeEventListener(event, oldNodeEvent as ((Event) -> Unit), null)
}
if (newNodeEvent != null) {
oldNode.addEventListener(event, newNodeEvent as ((Event) -> Unit), null)
oldNode.asDynamic()["event-$event"] = newNodeEvent
if (Komponent.logReplaceEvent) {
console.log("Set event $event on", oldNode)
}
oldNode.setEvent(event, newNodeEvent as ((Event) -> Unit))
}
oldEvents.remove(event)
}
@@ -261,17 +312,41 @@ object DiffPatch {
private fun replaceNode(oldNode: Node, newNode: Node) {
oldNode.parentNode?.also { parent ->
val clone = newNode.cloneNode(true)
if (newNode is HTMLElement) {
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)
}
}
}
cloneEvents(clone, newNode)
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
@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")) {
name.substring(2)
} else {
@@ -103,19 +103,19 @@ class HtmlBuilder(
}
}
tag.attributesEntries.forEach {
if (Komponent.updateStrategy == UpdateStrategy.DOM_DIFF) {
val key_value = "${it.key}-${it.value}"
hash = hash * 37 + key_value.hashCode()
}
if (it.key == "class") {
val classes = it.value.split(Regex("\\s+"))
for ((key, value) in tag.attributesEntries) {
if (key == "class") {
val classes = value.split(Regex("\\s+"))
val classNames = StringBuilder()
for (cls in classes) {
val cssStyle = komponent.declaredStyles[cls]
if (cssStyle != null) {
if (Komponent.updateStrategy == UpdateStrategy.DOM_DIFF) {
hash = hash * 37 + cssStyle.hashCode()
}
if (cls.endsWith(":hover")) {
val oldOnMouseOver = element.onmouseover
val oldOnMouseOut = element.onmouseout
@@ -138,15 +138,31 @@ class HtmlBuilder(
element.setStyles(cssStyle)
}
} else {
if (Komponent.updateStrategy == UpdateStrategy.DOM_DIFF) {
hash = hash * 37 + cls.hashCode()
}
classNames.append(cls)
classNames.append(" ")
}
}
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) {

View File

@@ -1,6 +1,5 @@
package nl.astraeus.komp
import kotlinx.html.Tag
import kotlinx.html.div
import org.w3c.dom.HTMLDivElement
import org.w3c.dom.HTMLElement
@@ -8,20 +7,21 @@ import org.w3c.dom.Node
import org.w3c.dom.css.CSSStyleDeclaration
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) {
component.update()
} else {
component.refresh()
}
val consumer = this.consumer
val element = component.element
if (consumer is HtmlBuilder && element != null) {
consumer.append(element)
component.element?.also {
append(it)
}
} else {
append(component.create())
}
}
@@ -47,10 +47,6 @@ abstract class Komponent {
consumer.render()
val result = consumer.finalize()
if (logReplaceEvent) {
console.log("Element hash", result, result.getKompHash())
}
element = 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")
}
}