From cbf76f18a2cc38cc510869ecb12812c97397027a Mon Sep 17 00:00:00 2001 From: rnentjes Date: Wed, 23 Feb 2022 21:40:57 +0100 Subject: [PATCH] Add update/replace option Took 1 hour 4 minutes --- .gitignore | 1 + build.gradle.kts | 2 +- .../nl/astraeus/komp/ElementExtentions.kt | 65 +++++++++-- .../kotlin/nl/astraeus/komp/HtmlBuilder.kt | 105 +++++++++++++++--- .../kotlin/nl/astraeus/komp/Komponent.kt | 10 ++ .../kotlin/nl/astraeus/komp/TestUpdate.kt | 5 +- 6 files changed, 157 insertions(+), 31 deletions(-) diff --git a/.gitignore b/.gitignore index 4dd2c58..49ccdc8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ local.properties *.ipr *.iws kotlin-js-store +.idea diff --git a/build.gradle.kts b/build.gradle.kts index 193ab54..e290d49 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - kotlin("multiplatform") version "1.6.10" + kotlin("multiplatform") version "1.6.20-M1" `maven-publish` signing id("org.jetbrains.dokka") version "1.5.31" diff --git a/src/jsMain/kotlin/nl/astraeus/komp/ElementExtentions.kt b/src/jsMain/kotlin/nl/astraeus/komp/ElementExtentions.kt index a362ee1..854a64f 100644 --- a/src/jsMain/kotlin/nl/astraeus/komp/ElementExtentions.kt +++ b/src/jsMain/kotlin/nl/astraeus/komp/ElementExtentions.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalStdlibApi::class) + package nl.astraeus.komp import org.w3c.dom.Element @@ -40,6 +42,14 @@ fun Element.printTree(indent: Int = 0): String { } result.append(") {") result.append("\n") + for ((name, event) in getKompEvents()) { + result.append(indent.asSpaces()) + result.append("on") + result.append(name) + result.append(" -> ") + result.append(event) + result.append("\n") + } for (index in 0 until childNodes.length) { childNodes[index]?.let { if (it is Element) { @@ -57,39 +67,61 @@ fun Element.printTree(indent: Int = 0): String { return result.toString() } -internal fun Element.setKompAttribute(name: String, value: String?) { -// val setAttrs: MutableSet = getKompAttributes() -// setAttrs.add(name) - //getNewAttributes().add(name) - +internal fun Element.setKompAttribute(attributeName: String, value: String?) { + //val attributeName = name.lowercase() if (value == null || value.isBlank()) { if (this is HTMLInputElement) { - checked = false + when (attributeName) { + "checked" -> { + checked = false + } +/* + "class" -> { + className = "" + } +*/ + "value" -> { + this.value = "" + } + else -> { + removeAttribute(attributeName) + } + } } else { - removeAttribute(name) + removeAttribute(attributeName) } } else { if (this is HTMLInputElement) { - when (name) { + when (attributeName) { "checked" -> { checked = "checked" == value } +/* "class" -> { className = value } +*/ "value" -> { this.value = value } else -> { - setAttribute(name, value) + setAttribute(attributeName, value) } } - } else if (this.getAttribute(name) != value) { - setAttribute(name, value) + } else if (this.getAttribute(attributeName) != value) { + setAttribute(attributeName, value) } } } +internal fun Element.clearKompEvents() { + val events = getKompEvents() + for ((name, event) in getKompEvents()) { + removeEventListener(name, event) + } + events.clear() +} + internal fun Element.setKompEvent(name: String, event: (Event) -> Unit) { val eventName: String = if (name.startsWith("on")) { name.substring(2) @@ -100,6 +132,17 @@ internal fun Element.setKompEvent(name: String, event: (Event) -> Unit) { this.addEventListener(eventName, event) } +internal fun Element.getKompEvents(): MutableMap Unit> { + var result: MutableMap Unit>? = this.asDynamic()["komp-events"] as MutableMap Unit>? + + if (result == null) { + result = mutableMapOf() + this.asDynamic()["komp-events"] = result + } + + return result +} + internal fun Element.findElementIndex(): Int { val childNodes = parentElement?.children if (childNodes != null) { diff --git a/src/jsMain/kotlin/nl/astraeus/komp/HtmlBuilder.kt b/src/jsMain/kotlin/nl/astraeus/komp/HtmlBuilder.kt index d21f2f7..05c7c5d 100644 --- a/src/jsMain/kotlin/nl/astraeus/komp/HtmlBuilder.kt +++ b/src/jsMain/kotlin/nl/astraeus/komp/HtmlBuilder.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalStdlibApi::class) + package nl.astraeus.komp import kotlinx.browser.document @@ -9,6 +11,7 @@ import kotlinx.html.TagConsumer import kotlinx.html.Unsafe import org.w3c.dom.Element import org.w3c.dom.HTMLElement +import org.w3c.dom.HTMLInputElement import org.w3c.dom.HTMLSpanElement import org.w3c.dom.Node import org.w3c.dom.asList @@ -28,7 +31,8 @@ fun FlowOrMetaDataOrPhrasingContent.currentElement(): Element = private data class ElementIndex( val parent: Node, - var childIndex: Int + var childIndex: Int, + var setAttr: MutableSet = mutableSetOf() ) private fun ArrayList.currentParent(): Node { @@ -49,6 +53,7 @@ private fun ArrayList.currentElement(): Node? { private fun ArrayList.nextElement() { this.lastOrNull()?.let { + it.setAttr.clear() it.childIndex++ } } @@ -72,14 +77,14 @@ private fun ArrayList.replace(new: Node) { private fun Node.asElement() = this as? HTMLElement class HtmlBuilder( - val komponent: Komponent?, + private val komponent: Komponent?, parent: Element, childIndex: Int = 0, ) : HtmlConsumer { private var currentPosition = arrayListOf() private var inDebug = false private var exceptionThrown = false - var currentNode: Node? = null + private var currentNode: Node? = null var root: Element? = null init { @@ -122,18 +127,18 @@ class HtmlBuilder( } } - private fun logReplace(msg: String) { + private fun logReplace(msg: () -> String) { if (Komponent.logReplaceEvent && inDebug) { - console.log(msg) + console.log(msg.invoke()) } } override fun onTagStart(tag: Tag) { - logReplace("onTagStart, [${tag.tagName}, ${tag.namespace}], currentPosition: $currentPosition") + logReplace { "onTagStart, [${tag.tagName}, ${tag.namespace}], currentPosition: $currentPosition" } currentNode = currentPosition.currentElement() if (currentNode == null) { - logReplace("onTagStart, currentNode1: $currentNode") + logReplace { "onTagStart, currentNode1: $currentNode" } currentNode = if (tag.namespace != null) { document.createElementNS(tag.namespace, tag.tagName) } else { @@ -142,9 +147,18 @@ class HtmlBuilder( //logReplace"onTagStart, currentElement1.1: $currentNode") currentPosition.currentParent().appendChild(currentNode!!) - } else { - logReplace("onTagStart, currentElement, namespace: ${currentNode?.asElement()?.namespaceURI} -> ${tag.namespace}") - logReplace("onTagStart, currentElement, replace: ${currentNode?.asElement()?.tagName} -> ${tag.tagName}") + } else if ( + Komponent.updateMode.isReplace || + ( + !currentNode?.asElement()?.tagName.equals(tag.tagName, true) || + ( + tag.namespace != null && + !currentNode?.asElement()?.namespaceURI.equals(tag.namespace, true) + ) + ) + ) { + logReplace { "onTagStart, currentElement, namespace: ${currentNode?.asElement()?.namespaceURI} -> ${tag.namespace}" } + logReplace { "onTagStart, currentElement, replace: ${currentNode?.asElement()?.tagName} -> ${tag.tagName}" } currentNode = if (tag.namespace != null) { document.createElementNS(tag.namespace, tag.tagName) @@ -155,6 +169,8 @@ class HtmlBuilder( currentPosition.replace(currentNode!!) } + check(currentNode == currentPosition.currentElement()) + currentElement = currentNode as? Element ?: currentElement if (currentNode is Element) { @@ -163,9 +179,14 @@ class HtmlBuilder( root = currentNode as Element } + if (Komponent.updateMode.isReplace) { + currentElement?.clearKompEvents() + } + for (entry in tag.attributesEntries) { - val attributeName = entry.key.lowercase() - currentElement!!.setKompAttribute(attributeName, entry.value) + currentElement!!.setKompAttribute(entry.key, entry.value) + console.log("onTagStart - set attribute", entry.key) + currentPosition.lastOrNull()?.setAttr?.add(entry.key) } if (tag.namespace != null) { @@ -190,19 +211,23 @@ class HtmlBuilder( } override fun onTagAttributeChange(tag: Tag, attribute: String, value: String?) { - logReplace("onTagAttributeChange, ${tag.tagName} [$attribute, $value]") + logReplace { "onTagAttributeChange, ${tag.tagName} [$attribute, $value]" } if (Komponent.enableAssertions) { checkTag(tag) } - val attributeName = attribute.lowercase() - - currentElement?.setKompAttribute(attributeName, value) + currentElement?.setKompAttribute(attribute, value) + if (value == null || value.isEmpty()) { + currentPosition.lastOrNull()?.setAttr?.remove(attribute) + } else { + console.log("onTagAttributeChange - set attribute", attribute) + currentPosition.lastOrNull()?.setAttr?.add(attribute) + } } override fun onTagEvent(tag: Tag, event: String, value: (Event) -> Unit) { - logReplace("onTagEvent, ${tag.tagName} [$event, $value]") + logReplace { "onTagEvent, ${tag.tagName} [$event, $value]" } if (Komponent.enableAssertions) { checkTag(tag) @@ -228,6 +253,48 @@ class HtmlBuilder( currentPosition.pop() + currentNode = currentPosition.currentElement() + currentElement = currentNode as? Element ?: currentElement + + if (currentElement != null) { + val setAttrs: Set = currentPosition.lastOrNull()?.setAttr ?: setOf() + + console.log("onTagEnd - set attr:", setAttrs.joinToString(",")) + + // remove attributes that where not set + val element = currentElement + if (element?.hasAttributes() == true) { + for (index in 0 until element.attributes.length) { + val attribute = element.attributes[index] + if (attribute?.name != null) { + val attr = attribute.name + + if ( + !setAttrs.contains(attr) && + attr != "style" + ) { + console.log("remove attr", attr) + if (element is HTMLInputElement) { + when (attr) { + "checked" -> { + element.checked = false + } + "value" -> { + element.value = "" + } + else -> { + element.removeAttribute(attr) + } + } + } else { + element.removeAttribute(attr) + } + } + } + } + } + } + currentPosition.nextElement() currentElement = currentElement?.parentElement as? HTMLElement @@ -331,9 +398,10 @@ class HtmlBuilder( exceptionThrown = true if (exception !is KomponentException) { + console.log("onTagError", tag, exception) val position = mutableListOf() var ce = currentElement - while(ce != null) { + while (ce != null) { position.add(ce) ce = ce.parentElement } @@ -351,6 +419,7 @@ class HtmlBuilder( } builder.append(" ") } + throw KomponentException( komponent, currentElement, diff --git a/src/jsMain/kotlin/nl/astraeus/komp/Komponent.kt b/src/jsMain/kotlin/nl/astraeus/komp/Komponent.kt index 428aa88..15e04d1 100644 --- a/src/jsMain/kotlin/nl/astraeus/komp/Komponent.kt +++ b/src/jsMain/kotlin/nl/astraeus/komp/Komponent.kt @@ -16,6 +16,15 @@ enum class UnsafeMode { UNSAFE_SVG_ONLY } +enum class UpdateMode { + REPLACE, + UPDATE, + ; + + val isReplace: Boolean get() { return this == REPLACE } + val isUpdate: Boolean get() { return this == UPDATE } +} + var Element.memoizeHash: String? get() { return getAttribute("memoize-hash") @@ -177,6 +186,7 @@ abstract class Komponent { var logRenderEvent = false var logReplaceEvent = false var enableAssertions = false + var updateMode = UpdateMode.REPLACE var unsafeMode = UnsafeMode.UNSAFE_DISABLED fun create(parent: HTMLElement, component: Komponent) { diff --git a/src/jsTest/kotlin/nl/astraeus/komp/TestUpdate.kt b/src/jsTest/kotlin/nl/astraeus/komp/TestUpdate.kt index ad82737..2e13289 100644 --- a/src/jsTest/kotlin/nl/astraeus/komp/TestUpdate.kt +++ b/src/jsTest/kotlin/nl/astraeus/komp/TestUpdate.kt @@ -2,6 +2,7 @@ package nl.astraeus.komp import kotlinx.browser.document import kotlinx.html.InputType +import kotlinx.html.classes import kotlinx.html.div import kotlinx.html.i import kotlinx.html.id @@ -37,6 +38,7 @@ class Child1 : Komponent() { class Child2 : Komponent() { override fun HtmlBuilder.render() { div { + id ="1234" +"Child 2" } } @@ -209,7 +211,8 @@ class TestUpdate { fun testCreate() { var elemTest: Element? = null val element = HtmlBuilder.create { - div("div_class") { + div(classes = "div_class") { + classes = classes + "bla'" id = "123" +"Test"