From d0442e785f6f8ef2f69b24cf59f068a06331612c Mon Sep 17 00:00:00 2001 From: rnentjes Date: Mon, 5 Apr 2021 17:21:45 +0200 Subject: [PATCH] VDom implementation --- komp.commonMain.iml | 16 +- komp.commonTest.iml | 18 +- komp.iml | 2 +- komp.ipr | 36 +- komp.jsMain.iml | 18 +- komp.jsTest.iml | 26 +- .../kotlin/nl/astraeus/komp/DiffPatch.kt | 330 +++++++----------- .../kotlin/nl/astraeus/komp/HtmlBuilder.kt | 297 +++++++++++----- .../kotlin/nl/astraeus/komp/Komponent.kt | 32 +- .../kotlin/nl/astraeus/komp/TestUpdate.kt | 64 ++-- 10 files changed, 459 insertions(+), 380 deletions(-) diff --git a/komp.commonMain.iml b/komp.commonMain.iml index 72350c6..c4f52a9 100644 --- a/komp.commonMain.iml +++ b/komp.commonMain.iml @@ -1,8 +1,8 @@ - + - + SOURCE_SET_HOLDER @@ -13,13 +13,13 @@ \ No newline at end of file diff --git a/komp.commonTest.iml b/komp.commonTest.iml index 719f2ec..4d4570e 100644 --- a/komp.commonTest.iml +++ b/komp.commonTest.iml @@ -1,8 +1,8 @@ - + - + SOURCE_SET_HOLDER jsLegacyBrowserTest|komp:jsTest|jsLegacy @@ -16,13 +16,13 @@ \ No newline at end of file diff --git a/komp.iml b/komp.iml index 6f0c251..6696c55 100644 --- a/komp.iml +++ b/komp.iml @@ -1,5 +1,5 @@ - + diff --git a/komp.ipr b/komp.ipr index aa6e2f4..1d16b4e 100644 --- a/komp.ipr +++ b/komp.ipr @@ -55,6 +55,12 @@ + + $PROJECT_DIR$/build/libs + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/komp.jsMain.iml b/komp.jsMain.iml index 59aae30..266512e 100644 --- a/komp.jsMain.iml +++ b/komp.jsMain.iml @@ -1,8 +1,8 @@ - + - + komp:commonMain komp.commonMain @@ -24,11 +24,11 @@ \ No newline at end of file diff --git a/komp.jsTest.iml b/komp.jsTest.iml index 0741fb6..429f5ff 100644 --- a/komp.jsTest.iml +++ b/komp.jsTest.iml @@ -1,12 +1,12 @@ - + - + komp:commonTest - komp.jsMain komp.commonTest + komp.jsMain komp.commonMain COMPILATION_AND_SOURCE_SET_HOLDER @@ -29,11 +29,11 @@ \ No newline at end of file diff --git a/src/jsMain/kotlin/nl/astraeus/komp/DiffPatch.kt b/src/jsMain/kotlin/nl/astraeus/komp/DiffPatch.kt index 64b30fc..29892d0 100644 --- a/src/jsMain/kotlin/nl/astraeus/komp/DiffPatch.kt +++ b/src/jsMain/kotlin/nl/astraeus/komp/DiffPatch.kt @@ -1,297 +1,205 @@ package nl.astraeus.komp -import org.w3c.dom.* -import org.w3c.dom.events.Event - -const val HASH_VALUE = "komp-hash-value" - -//const val HASH_ATTRIBUTE = "data-komp-hash" -const val EVENT_PROPERTY = "komp-events" - -fun Node.getKompHash(): Int = this.asDynamic()[HASH_VALUE] as? Int? ?: -1 - -fun Node.setKompHash(hash: Int) { - this.asDynamic()[HASH_VALUE] = hash -} - -private fun NodeList.findNodeHashIndex(hash: Int): Int { - for (index in 0..this.length) { - val node = this[index] - if (node is HTMLElement && node.getKompHash() == hash) { - return index - } - } - - return -1 -} +import org.w3c.dom.HTMLElement +import org.w3c.dom.HTMLInputElement +import org.w3c.dom.Node +import org.w3c.dom.get object DiffPatch { - fun hashesMatch(oldNode: Node, newNode: Node): Boolean { - return ( - oldNode is HTMLElement && - newNode is HTMLElement && - oldNode.nodeName == newNode.nodeName && - oldNode.getKompHash() != -1 && - newNode.getKompHash() != -1 && - oldNode.getKompHash() == newNode.getKompHash() - ) - } - - private fun updateKomponentOnNode(oldNode: Node, newNode: Node) { - val komponent = newNode.asDynamic()[KOMP_KOMPONENT] as? Komponent + private fun updateKomponentOnNode(element: Node, newVdom: VDOMElement) { + val komponent = newVdom.komponent if (komponent != null) { if (Komponent.logReplaceEvent) { - console.log("Keeping oldNode, set oldNode element on Komponent", oldNode, komponent) + console.log("Keeping oldNode, set oldNode element on Komponent", element, komponent) } - komponent.element = oldNode + komponent.element = element } } - fun updateNode(oldNode: Node, newNode: Node): Node { - if (hashesMatch(oldNode, newNode)) { + fun updateNode(element: Node, oldVdom: VDOMElement, newVdom: VDOMElement): Node { + if (oldVdom.hash == newVdom.hash) { if (Komponent.logReplaceEvent) { - console.log("Hashes match", oldNode, newNode, oldNode.getKompHash(), newNode.getKompHash()) + console.log("Hashes match", oldVdom, newVdom, oldVdom.hash, newVdom.hash) } - updateKomponentOnNode(oldNode, newNode) - return oldNode + // no change + return element } - if (oldNode.nodeType == newNode.nodeType && oldNode.nodeType == 3.toShort()) { - if (oldNode.textContent != newNode.textContent) { + if (oldVdom.type == newVdom.type && oldVdom.type == VDOMElementType.TEXT) { + if (oldVdom.content != newVdom.content) { if (Komponent.logReplaceEvent) { - console.log("Updating text content", oldNode, newNode) + console.log("Updating text content", oldVdom, newVdom) } - oldNode.textContent = newNode.textContent + element.textContent = newVdom.content } - updateKomponentOnNode(oldNode, newNode) - return oldNode + return element } - if (oldNode is HTMLElement && newNode is HTMLElement) { - if (oldNode.nodeName == newNode.nodeName) { + if (oldVdom.type == newVdom.type && oldVdom.type == VDOMElementType.TAG) { + if (oldVdom.content == newVdom.content) { if (Komponent.logReplaceEvent) { - console.log("Update attributes", oldNode.nodeName, newNode.nodeName) + console.log("Update attributes", oldVdom.content, newVdom.content) } - updateAttributes(oldNode, newNode); + updateAttributes(element as HTMLElement, oldVdom, newVdom) if (Komponent.logReplaceEvent) { - console.log("Update events", oldNode.nodeName, newNode.nodeName) + console.log("Update events", oldVdom.content, newVdom.content) } - updateEvents(oldNode, newNode) + updateEvents(element as HTMLElement, oldVdom, newVdom) if (Komponent.logReplaceEvent) { - console.log("Update children", oldNode.nodeName, newNode.nodeName) + console.log("Update children", oldVdom.content, newVdom.content) } - updateKomponentOnNode(oldNode, newNode) - updateChildren(oldNode, newNode) - oldNode.setKompHash(newNode.getKompHash()) + updateKomponentOnNode(element, newVdom) + updateChildren(element, oldVdom, newVdom) - return oldNode + return element } } if (Komponent.logReplaceEvent) { - console.log("Replace node (type)", oldNode.nodeType, oldNode, newNode) + console.log("Replace node (type)", newVdom.type, oldVdom, newVdom) } - oldNode.parentNode?.replaceChild(newNode, oldNode) + val newNode = newVdom.createElement() + updateKomponentOnNode(newNode, newVdom) + element.parentNode?.replaceChild(newNode, element) return newNode } - private fun updateAttributes(oldNode: HTMLElement, newNode: HTMLElement) { + private fun updateAttributes(element: HTMLElement, oldVdom: VDOMElement, newVdom: VDOMElement) { // removed attributes - for (name in oldNode.getAttributeNames()) { - val attr = oldNode.attributes[name] - - if (attr != null && newNode.getAttribute(name) == null) { - oldNode.removeAttribute(name) + for ((name, attr) in oldVdom.attributes) { + if (newVdom.attributes[name] == null) { + element.removeAttribute(name) } } - for (name in newNode.getAttributeNames()) { - val value = newNode.getAttribute(name) - val oldValue = oldNode.getAttribute(name) + for ((name, value) in newVdom.attributes) { + val oldValue = oldVdom.attributes[name] if (value != oldValue) { - if (value != null) { - oldNode.setAttribute(name, value) - }else { - oldNode.removeAttribute(name) - } + element.setAttribute(name, value) } } - if (newNode is HTMLInputElement && oldNode is HTMLInputElement) { - oldNode.value = newNode.value - oldNode.checked = newNode.checked + if (newVdom.content == "input" && oldVdom.content == "input") { + if (element is HTMLInputElement) { + element.value = newVdom.attributes["value"] ?: "" + element.checked = newVdom.attributes["checked"] == "true" + } } } - private fun updateChildren(oldNode: HTMLElement, newNode: HTMLElement) { + private fun updateChildren( + element: HTMLElement, + oldVdom: VDOMElement, + newVdom: VDOMElement + ) { var oldIndex = 0 var newIndex = 0 if (Komponent.logReplaceEvent) { console.log( - "updateChildren HTML old(${oldNode.childNodes.length})", - oldNode.innerHTML + "updateChildren HTML old(${oldVdom.childNodes.size})", + oldVdom.toString() ) console.log( - "updateChildren HTML new(${newNode.childNodes.length})", - newNode.innerHTML + "updateChildren HTML new(${newVdom.childNodes.size})", + newVdom.toString() ) } - while (newIndex < newNode.childNodes.length) { + while (oldIndex < oldVdom.childNodes.size && newIndex < newVdom.childNodes.size) { if (Komponent.logReplaceEvent) { console.log("Update Old/new", oldIndex, newIndex) } - val newChildNode = newNode.childNodes[newIndex] + val oldChildNode = oldVdom.childNodes[oldIndex] + val newChildNode = newVdom.childNodes[newIndex] - if (oldIndex < oldNode.childNodes.length) { - val oldChildNode = oldNode.childNodes[oldIndex] - - if (oldChildNode != null && newChildNode != null) { - /* - if (Komponent.logReplaceEvent) { - console.log(">>> updateChildren old/new", oldChildNode, newChildNode) - } - */ - - if (Komponent.logReplaceEvent) { - console.log("Update node Old/new", oldChildNode, newChildNode) - } - - if (!hashesMatch(oldChildNode, newChildNode) && newChildNode is HTMLElement && oldChildNode is HTMLElement) { - if (Komponent.logReplaceEvent) { - console.log("Hashes don't match") - } - - val oldHash = oldChildNode.getKompHash() - val newHash = newChildNode.getKompHash() - - if (newHash >= 0) { - val oldNodeWithNewHashIndex = oldNode.childNodes.findNodeHashIndex(newHash) - - if (Komponent.logReplaceEvent) { - console.log("oldNodeWithNewHashIndex", newHash, oldNodeWithNewHashIndex) - } - - if (oldNodeWithNewHashIndex > oldIndex) { - if (oldHash >= 0) { - val newNodeWithOldHashIndex = newNode.childNodes.findNodeHashIndex(oldHash) - - // remove i.o. swap - if (newNodeWithOldHashIndex == -1) { - if (Komponent.logReplaceEvent) { - console.log("Old node missing in new tree, remove node", oldChildNode) - } - oldNode.removeChild(oldChildNode) - continue - } - } - val nodeWithHash = oldNode.childNodes[oldNodeWithNewHashIndex] - - if (Komponent.logReplaceEvent) { - console.log("nodeWithHash", nodeWithHash) - } - if (nodeWithHash != null) { - if (Komponent.logReplaceEvent) { - console.log(">-> swap nodes", oldNode) - } - - oldNode.insertBefore(nodeWithHash, oldNode.childNodes[oldIndex]) - - if (Komponent.logReplaceEvent) { - console.log(">-> swapped nodes", oldNode) - } - newIndex++ - oldIndex++ - continue - } - } else if (oldHash >= 0 && newNode.childNodes.findNodeHashIndex(oldHash) > newIndex) { - if (Komponent.logReplaceEvent) { - console.log("newNodeWithOldHashIndex", oldHash, newNode.childNodes.findNodeHashIndex(oldHash)) - } - - oldNode.insertBefore(newChildNode, oldChildNode) - oldIndex++ - continue - } - } - } - - 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++ + if (Komponent.logReplaceEvent) { + console.log("Update node Old/new", oldChildNode, newChildNode) } - /* - if (Komponent.logReplaceEvent) { - console.log("<<< Updated Old/new", oldNode.innerHTML, newNode.innerHTML) - } - */ + // scenarios: + // - hashes match, next + // - hashes don't match: + // -- old hash is down in new list + // --- delta == 1, insert new node + // -- new hash is down in old list + // --- delta == 1, remove current else swap? + // else: replace current node with new + + if (oldVdom.hash != newVdom.hash && + newChildNode.type == VDOMElementType.TAG && + oldChildNode.type == VDOMElementType.TAG + ) { + if (Komponent.logReplaceEvent) { + console.log("Hashes don't match") + } + + val oldHash = oldChildNode.hash.hashCode() + val newHash = newChildNode.hash.hashCode() + + val newHashIndexInOld = oldVdom.findNodeHashIndex(newHash) + val oldHashIndexInNew = newVdom.findNodeHashIndex(oldHash) + + if (newHashIndexInOld == oldIndex + 1 && oldHashIndexInNew == newIndex) { + // remove + element.removeChild(element.childNodes[oldIndex]!!) + oldIndex++ + continue + } else if (newHashIndexInOld == oldIndex && oldHashIndexInNew == newIndex + 1) { + // insert + element.insertBefore(newChildNode.createElement(), element.childNodes[oldIndex]!!) + newIndex++ + oldIndex++ + continue + } + } + + // update + updateNode(element.childNodes[oldIndex]!!, oldChildNode, newChildNode) + oldIndex++ + newIndex++ } - while (oldIndex < oldNode.childNodes.length) { - oldNode.childNodes[oldIndex]?.also { + while (element.childNodes.length > newVdom.childNodes.size) { + element.childNodes[element.childNodes.length - 1]?.also { if (Komponent.logReplaceEvent) { console.log("Remove old node", it) } - oldNode.removeChild(it) + element.removeChild(it) } } + + while (newIndex < newVdom.childNodes.size) { + newVdom.childNodes[newIndex].also { + element.appendChild(it.createElement()) + } + newIndex++ + } } - private fun updateEvents(oldNode: HTMLElement, newNode: HTMLElement) { - val oldEvents = (oldNode.asDynamic()[EVENT_PROPERTY] as? MutableList) ?: mutableListOf() - val newEvents = (newNode.asDynamic()[EVENT_PROPERTY] as? MutableList) ?: mutableListOf() + private fun updateEvents(element: HTMLElement, oldVdom: VDOMElement, newVdom: VDOMElement) { + val oldEvents = oldVdom.events + val newEvents = newVdom.events if (Komponent.logReplaceEvent) { - console.log("Update events", oldNode.getAttribute(EVENT_PROPERTY), newNode.getAttribute(EVENT_PROPERTY)) + console.log("Update events", oldEvents, newEvents) } - for (event in oldEvents) { - oldNode.removeKompEvent(event) + for ((name, event) in oldEvents) { + element.removeEventListener(name, event) } - for (event in newEvents) { - val newNodeEvent = newNode.asDynamic()["event-$event"] - - if (newNodeEvent != null) { - if (Komponent.logReplaceEvent) { - console.log("Set event $event on", oldNode) - } - oldNode.setKompEvent(event, newNodeEvent as ((Event) -> Unit)) + for ((name, event) in newEvents) { + if (Komponent.logReplaceEvent) { + console.log("Set event $event on", element) } - } - - if (newEvents.isEmpty()) { - oldNode.asDynamic()[EVENT_PROPERTY] = null - } else { - oldNode.asDynamic()[EVENT_PROPERTY] = newNode.asDynamic()[EVENT_PROPERTY] + element.addEventListener(name, event) } } diff --git a/src/jsMain/kotlin/nl/astraeus/komp/HtmlBuilder.kt b/src/jsMain/kotlin/nl/astraeus/komp/HtmlBuilder.kt index 518865d..89932f6 100644 --- a/src/jsMain/kotlin/nl/astraeus/komp/HtmlBuilder.kt +++ b/src/jsMain/kotlin/nl/astraeus/komp/HtmlBuilder.kt @@ -2,57 +2,223 @@ package nl.astraeus.komp import kotlinx.browser.document import kotlinx.html.* -import org.w3c.dom.* +import org.w3c.dom.Element +import org.w3c.dom.Node import org.w3c.dom.events.Event +import kotlin.collections.MutableList +import kotlin.collections.MutableMap +import kotlin.collections.arrayListOf +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.isNotEmpty +import kotlin.collections.iterator +import kotlin.collections.last +import kotlin.collections.lastIndex +import kotlin.collections.mutableListOf +import kotlin.collections.mutableMapOf +import kotlin.collections.set +import kotlin.collections.withIndex -@Suppress("NOTHING_TO_INLINE") -inline fun HTMLElement.setKompEvent(name: String, noinline callback: (Event) -> Unit) { - val eventName = if (name.startsWith("on")) { - name.substring(2) - } else { - name - } - addEventListener(eventName, callback, null) +private fun attributeHash(key: String, value: String): Int = + 3 * key.hashCode() + + 5 * value.hashCode() - val events: MutableList = (asDynamic()[EVENT_PROPERTY] as? MutableList) ?: mutableListOf() +private fun MutableMap.kompHash(): Int { + var result = 0 - events.add(eventName) - asDynamic()[EVENT_PROPERTY] = events - asDynamic()["event-$eventName"] = callback -} - -@Suppress("NOTHING_TO_INLINE") -inline fun HTMLElement.removeKompEvent(name: String) { - val eventName = if (name.startsWith("on")) { - name.substring(2) - } else { - name + for ((name, value) in this) { + result += attributeHash(name, value) } - removeEventListener(eventName, asDynamic()["event-$eventName"] as ((Event) -> Unit), null) - - val events: MutableList = (asDynamic()[EVENT_PROPERTY] as? MutableList) ?: mutableListOf() - - events.remove(eventName) - asDynamic()["event-$eventName"] = null + return result } -interface HtmlConsumer : TagConsumer { - fun append(node: Node) +private fun MutableMap Unit>.kompHash(): Int { + var result = 0 + + for ((name, event) in this) { + result += attributeHash(name, event.toString()) + } + + return result +} + +private fun MutableList.kompHash(): Int { + var result = 0 + + for (vdom in this) { + result += 3 * vdom.hash.hashCode() + } + + return result +} + +enum class VDOMElementType { + TAG, + TEXT, + ENTITY, + UNSAFE, + COMMENT +} + +class VDOMElementHash( + var baseHash: Int, + var contentHash: Int, + var typeHash: Int, + var namespaceHash: Int = 0, + var attributesHash: Int = 0, + var eventsHash: Int = 0, + var childNodesHash: Int = 0 +) { + + override fun hashCode(): Int = baseHash + + 3 * contentHash + + 5 * typeHash + + 7 * namespaceHash + + 11 * attributesHash + + 13 * eventsHash + + 15 * childNodesHash + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is VDOMElementHash) return false + + return other.hashCode() == this.hashCode() + } + +} + +class VDOMElement( + val baseHash: Int, + var content: String, + var namespace: String? = null, + var type: VDOMElementType = VDOMElementType.TAG, +) { + val attributes: MutableMap = mutableMapOf() + val events: MutableMap Unit> = mutableMapOf() + val childNodes: MutableList = mutableListOf() + + val hash = VDOMElementHash( + baseHash, + content.hashCode(), + type.hashCode() + ) + + var id: String = "" + set(value) { + field = value + attributes["id"] = value + } + var komponent: Komponent? = null + + fun setKompEvent(event: String, value: (Event) -> Unit) { + val eventName = if (event.startsWith("on")) { + event.substring(2) + } else { + event + } + val recalculate = events.containsKey(eventName) + events[eventName] = value + if (recalculate) { + hash.eventsHash = events.kompHash() + } else { + hash.eventsHash += attributeHash(eventName, value.toString()) + } + } + + fun appendChild(element: VDOMElement) { + childNodes.add(element) + //hash.childNodesHash += element.hash.hashCode() + } + + fun updateChildHash() { + hash.childNodesHash = childNodes.kompHash() + } + + fun removeAttribute(attr: String) { + if (attributes.containsKey(attr)) { + hash.attributesHash -= attributeHash(attr, attributes[attr] ?: "") + } + attributes.remove(attr) + } + + fun setAttribute(attr: String, value: String) { + if (attributes.containsKey(attr)) { + hash.attributesHash -= attributeHash(attr, attributes[attr] ?: "") + } + if (attr.toLowerCase() == "id") { + id = value + } + attributes[attr] = value + hash.attributesHash += attributeHash(attr, value) + } + + fun findNodeHashIndex(hash: Int): Int { + for ((index, node) in this.childNodes.withIndex()) { + if (node.type == VDOMElementType.TAG && node.hash.hashCode() == hash) { + return index + } + } + + return -2 + } + + fun createElement(): Node { + val result = when (type) { + VDOMElementType.TAG -> { + val result: Element = if (namespace != null) { + document.createElementNS(namespace, content) + } else { + document.createElement(content) + } + + for ((name, value) in attributes) { + result.setAttribute(name, value) + } + + for ((name, value) in events) { + result.addEventListener(name, value) + } + + for (child in childNodes) { + result.appendChild(child.createElement()) + } + + result + } + VDOMElementType.ENTITY, + VDOMElementType.UNSAFE, + VDOMElementType.TEXT -> { + document.createTextNode(content) + } + VDOMElementType.COMMENT -> { + document.createComment(content) + } + } + + komponent?.also { + it.element = result + } + + return result + } +} + +interface HtmlConsumer : TagConsumer { + fun append(node: VDOMElement) } class HtmlBuilder( - val komponent: Komponent, - val document: Document, val baseHash: Int ) : HtmlConsumer { - private val path = arrayListOf() - private var lastLeaved: HTMLElement? = null + private val path = arrayListOf() + private var lastLeaved: VDOMElement? = null override fun onTagStart(tag: Tag) { - val element: HTMLElement = when { - tag.namespace != null -> document.createElementNS(tag.namespace!!, tag.tagName).asDynamic() - else -> document.createElement(tag.tagName) as HTMLElement + val element = VDOMElement(baseHash, tag.tagName, tag.namespace) + + for (entry in tag.attributesEntries) { + element.setAttribute(entry.key, entry.value) } if (path.isNotEmpty()) { @@ -64,9 +230,9 @@ class HtmlBuilder( override fun onTagAttributeChange(tag: Tag, attribute: String, value: String?) { when { - path.isEmpty() -> throw IllegalStateException("No current tag") - path.last().tagName.toLowerCase() != tag.tagName.toLowerCase() -> throw IllegalStateException("Wrong current tag") - else -> path.last().let { node -> + path.isEmpty() -> throw IllegalStateException("No current tag") + path.last().content.toLowerCase() != tag.tagName.toLowerCase() -> throw IllegalStateException("Wrong current tag") + else -> path.last().let { node -> if (value == null) { node.removeAttribute(attribute) } else { @@ -78,38 +244,19 @@ class HtmlBuilder( override fun onTagEvent(tag: Tag, event: String, value: (Event) -> Unit) { when { - path.isEmpty() -> throw IllegalStateException("No current tag") - path.last().tagName.toLowerCase() != tag.tagName.toLowerCase() -> throw IllegalStateException("Wrong current tag") + path.isEmpty() -> throw IllegalStateException("No current tag") + path.last().content.toLowerCase() != tag.tagName.toLowerCase() -> throw IllegalStateException("Wrong current tag") else -> path.last().setKompEvent(event, value) } } override fun onTagEnd(tag: Tag) { - var hash = 0 - if (path.isEmpty() || path.last().tagName.toLowerCase() != tag.tagName.toLowerCase()) { + if (path.isEmpty() || path.last().content.toLowerCase() != tag.tagName.toLowerCase()) { throw IllegalStateException("We haven't entered tag ${tag.tagName} but trying to leave") } - val element = path.last() - - for (index in 0 until element.childNodes.length) { - val child = element.childNodes[index] - if (child is HTMLElement) { - hash = hash * 37 + child.getKompHash() - } else { - hash = hash * 37 + (child?.textContent?.hashCode() ?: 0) - } - } - - for ((key, value) in tag.attributesEntries) { - element.setAttribute(key, value) - - val key_value = "${key}-${value}" - hash = hash * 37 + key_value.hashCode() - } - - element.setKompHash(baseHash * 53 + hash) lastLeaved = path.removeAt(path.lastIndex) + lastLeaved?.updateChildHash() } override fun onTagContent(content: CharSequence) { @@ -117,7 +264,7 @@ class HtmlBuilder( throw IllegalStateException("No current DOM node") } - path.last().appendChild(document.createTextNode(content.toString())) + path.last().appendChild(VDOMElement(baseHash, content.toString(), type = VDOMElementType.TEXT)) } override fun onTagContentEntity(entity: Entities) { @@ -126,15 +273,10 @@ class HtmlBuilder( } // stupid hack as browsers don't support createEntityReference - val s = document.createElement("span") as HTMLElement - s.innerHTML = entity.text - path.last().appendChild(s.childNodes.asList().first { it.nodeType == Node.TEXT_NODE }) - - // other solution would be - // pathLast().innerHTML += entity.text + path.last().appendChild(VDOMElement(baseHash, entity.text, type = VDOMElementType.ENTITY)) } - override fun append(node: Node) { + override fun append(node: VDOMElement) { path.last().appendChild(node) } @@ -142,7 +284,7 @@ class HtmlBuilder( with(DefaultUnsafe()) { block() - path.last().innerHTML += toString() + path.last().appendChild(VDOMElement(baseHash, toString(), type = VDOMElementType.UNSAFE)) } } @@ -151,18 +293,17 @@ class HtmlBuilder( throw IllegalStateException("No current DOM node") } - path.last().appendChild(document.createComment(content.toString())) + path.last().appendChild(VDOMElement(baseHash, content.toString(), type = VDOMElementType.COMMENT)) } - override fun finalize(): HTMLElement = lastLeaved?.asR() ?: throw IllegalStateException("We can't finalize as there was no tags") - - @Suppress("UNCHECKED_CAST") - private fun HTMLElement.asR(): HTMLElement = this.asDynamic() + override fun finalize(): VDOMElement { + return lastLeaved ?: throw IllegalStateException("We can't finalize as there was no tags") + } companion object { - fun create(content: HtmlBuilder.() -> Unit): HTMLElement { + fun create(content: HtmlBuilder.() -> Unit): VDOMElement { val komponent = DummyKomponent() - val consumer = HtmlBuilder(komponent, document, komponent.hashCode()) + val consumer = HtmlBuilder(komponent.hashCode()) content.invoke(consumer) return consumer.finalize() } diff --git a/src/jsMain/kotlin/nl/astraeus/komp/Komponent.kt b/src/jsMain/kotlin/nl/astraeus/komp/Komponent.kt index f5752d5..2bf0e6b 100644 --- a/src/jsMain/kotlin/nl/astraeus/komp/Komponent.kt +++ b/src/jsMain/kotlin/nl/astraeus/komp/Komponent.kt @@ -9,8 +9,6 @@ import org.w3c.dom.Node import org.w3c.dom.css.CSSStyleDeclaration import kotlin.reflect.KProperty -const val KOMP_KOMPONENT = "komp-komponent" - typealias CssStyle = CSSStyleDeclaration.() -> Unit class StateDelegate( @@ -49,11 +47,12 @@ abstract class Komponent { private var createIndex = getNextCreateIndex() private var dirty: Boolean = true + var vdom: VDOMElement? = null var element: Node? = null val declaredStyles: MutableMap = HashMap() - open fun create(): HTMLElement { - val consumer = HtmlBuilder(this, document, this.createIndex) + open fun create(): VDOMElement { + val consumer = HtmlBuilder(this.createIndex) try { consumer.render() } catch (e: Throwable) { @@ -67,8 +66,9 @@ abstract class Komponent { result.id = "komp_${createIndex}" } - element = result - element.asDynamic()[KOMP_KOMPONENT] = this + result.komponent = this + + vdom = result dirty = false @@ -96,20 +96,26 @@ abstract class Komponent { } internal fun refresh() { - val oldElement = element + val oldElement = vdom if (logRenderEvent) { console.log("Rendering", this) } val newElement = create() - if (oldElement != null) { + element = if (oldElement != null && element != null) { if (logReplaceEvent) { console.log("DomDiffing", oldElement, newElement) } - element = DiffPatch.updateNode(oldElement, newElement) + DiffPatch.updateNode(element!!, oldElement, newElement) + } else { + if (logReplaceEvent) { + console.log("Create", newElement) + } + newElement.createElement() } + vdom = newElement dirty = false } @@ -122,7 +128,9 @@ abstract class Komponent { var logReplaceEvent = false fun create(parent: HTMLElement, component: Komponent, insertAsFirst: Boolean = false) { - val element = component.create() + val vdomElement = component.create() + val element = vdomElement.createElement() + component.element = element if (insertAsFirst && parent.childElementCount > 0) { parent.insertBefore(element, parent.firstChild) @@ -165,7 +173,11 @@ abstract class Komponent { console.log("Skip ${next.createIndex}") } } + } else { + console.log("Komponent element has no id, ", next, element) } + } else { + console.log("Komponent element is null", next) } } diff --git a/src/jsTest/kotlin/nl/astraeus/komp/TestUpdate.kt b/src/jsTest/kotlin/nl/astraeus/komp/TestUpdate.kt index 39da898..6bb5610 100644 --- a/src/jsTest/kotlin/nl/astraeus/komp/TestUpdate.kt +++ b/src/jsTest/kotlin/nl/astraeus/komp/TestUpdate.kt @@ -2,46 +2,49 @@ package nl.astraeus.komp import kotlinx.html.* import kotlinx.html.js.onClickFunction +import org.w3c.dom.Element 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) { +fun nodesEqual(element: Element, vdom: VDOMElement): Boolean { + if (element.childNodes.length != vdom.childNodes.size) { return false } - if (node1 is HTMLElement && node2 is HTMLElement) { - if (node1.attributes.length != node2.attributes.length) { + if (element.attributes.length != vdom.attributes.size) { + return false + } + for ((name, value) in vdom.attributes) { + if (value != element.getAttribute(name)) { return false } - for (index in 0 until node1.attributes.length) { - node1.attributes[index]?.also { attr1 -> - val attr2 = node2.getAttribute(attr1.name) + } + for ((index, child) in vdom.childNodes.withIndex()) { + if (index < element.childNodes.length) { + val elementChild = element.childNodes[index] - if (attr1.value != attr2) { + if (child.type == VDOMElementType.TAG) { + if (!nodesEqual(elementChild as HTMLElement, child)) { + return false + } + } else if (child.type == VDOMElementType.TEXT) { + if (child.content != element.textContent) { 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 - } - } - } + } else { + return false } } + return true } class TestUpdate { @Test - fun testCompare1() { + fun testCompare() { val dom1 = HtmlBuilder.create { div { div(classes = "bla") { @@ -72,9 +75,13 @@ class TestUpdate { } } - DiffPatch.updateNode(dom1, dom2) + var element = dom1.createElement() - assertTrue(nodesEqual(dom1, dom2), "Updated dom not equal to original") + assertTrue(nodesEqual(element as Element, dom1), "Created dom not equal to original") + + element = DiffPatch.updateNode(element, dom1, dom2) as HTMLElement + + assertTrue(nodesEqual(element, dom2), "Updated dom not equal to original") } @Test @@ -88,7 +95,7 @@ class TestUpdate { table { tr { th { - + "Header" + +"Header" } } tr { @@ -105,7 +112,7 @@ class TestUpdate { div { div { span { - + "Other text" + +"Other text" } } span { @@ -123,10 +130,15 @@ class TestUpdate { } } - Komponent.logReplaceEvent = true - DiffPatch.updateNode(dom1, dom2) + Komponent.logRenderEvent = true - assertTrue(nodesEqual(dom1, dom2), "Updated dom not equal to original") + var element = dom1.createElement() + + assertTrue(nodesEqual(element as Element, dom1), "Created dom not equal to original") + + element = DiffPatch.updateNode(element, dom1, dom2) as HTMLElement + + assertTrue(nodesEqual(element, dom2), "Updated dom not equal to original") } }