From 66b3fb3c220f66098d5f5b6fbb7617346d6db204 Mon Sep 17 00:00:00 2001 From: rnentjes Date: Wed, 10 Feb 2021 13:20:07 +0100 Subject: [PATCH] Merge remember branch, Update to 0.2.5-SNAPSHOT --- build.gradle.kts | 11 +- komp.commonMain.iml | 38 +++++++ komp.commonTest.iml | 44 ++++++++ komp.iml | 2 +- komp.ipr | 51 ++++++++- komp.jsMain.iml | 59 ++++++++++ komp.jsTest.iml | 70 ++++++++++++ komp_main.iml | 48 -------- komp_test.iml | 67 ----------- .../kotlin/nl/astraeus/komp/DiffPatch.kt | 30 +++-- .../kotlin/nl/astraeus/komp/HtmlBuilder.kt | 6 +- .../kotlin/nl/astraeus/komp/Komponent.kt | 106 +++++++++++++++++- 12 files changed, 389 insertions(+), 143 deletions(-) create mode 100644 komp.commonMain.iml create mode 100644 komp.commonTest.iml create mode 100644 komp.jsMain.iml create mode 100644 komp.jsTest.iml delete mode 100644 komp_main.iml delete mode 100644 komp_test.iml diff --git a/build.gradle.kts b/build.gradle.kts index 99db642..be0cea5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,10 +1,11 @@ + plugins { - kotlin("multiplatform") version "1.3.71" + kotlin("multiplatform") version "1.4.30" `maven-publish` } group = "nl.astraeus" -version = "0.1.21-SNAPSHOT" +version = "0.2.5-SNAPSHOT" repositories { mavenCentral() @@ -15,7 +16,7 @@ kotlin { /* Targets configuration omitted. * To find out how to configure the targets, please follow the link: * https://kotlinlang.org/docs/reference/building-mpp-with-gradle.html#setting-up-targets */ - js { + js(BOTH) { browser { //produceKotlinLibrary() testTask { @@ -31,14 +32,12 @@ kotlin { dependencies { implementation(kotlin("stdlib-common")) - //implementation("org.jetbrains.kotlinx:kotlinx-html:0.7.2-build-1711") + api("org.jetbrains.kotlinx:kotlinx-html-js:0.7.2") } } val jsMain by getting { dependencies { implementation(kotlin("stdlib-js")) - - api("org.jetbrains.kotlinx:kotlinx-html-js:0.7.1") } } val jsTest by getting { diff --git a/komp.commonMain.iml b/komp.commonMain.iml new file mode 100644 index 0000000..72350c6 --- /dev/null +++ b/komp.commonMain.iml @@ -0,0 +1,38 @@ + + + + + + SOURCE_SET_HOLDER + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/komp.commonTest.iml b/komp.commonTest.iml new file mode 100644 index 0000000..719f2ec --- /dev/null +++ b/komp.commonTest.iml @@ -0,0 +1,44 @@ + + + + + + SOURCE_SET_HOLDER + + jsLegacyBrowserTest|komp:jsTest|jsLegacy + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/komp.iml b/komp.iml index b4be323..6f0c251 100644 --- a/komp.iml +++ b/komp.iml @@ -1,5 +1,5 @@ - + diff --git a/komp.ipr b/komp.ipr index 46cb162..fbae6c3 100644 --- a/komp.ipr +++ b/komp.ipr @@ -13,6 +13,48 @@ + + $PROJECT_DIR$/build/libs + + + + + + $PROJECT_DIR$/build/libs + + + + + + $PROJECT_DIR$/build/libs + + + + + + $PROJECT_DIR$/build/libs + + + + + + $PROJECT_DIR$/build/libs + + + + + + $PROJECT_DIR$/build/libs + + + + + + $PROJECT_DIR$/build/libs + + + + - @@ -216,7 +256,7 @@ - + @@ -254,6 +294,11 @@ diff --git a/komp.jsMain.iml b/komp.jsMain.iml new file mode 100644 index 0000000..59aae30 --- /dev/null +++ b/komp.jsMain.iml @@ -0,0 +1,59 @@ + + + + + + komp:commonMain + + komp.commonMain + + COMPILATION_AND_SOURCE_SET_HOLDER + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/komp.jsTest.iml b/komp.jsTest.iml new file mode 100644 index 0000000..e8b019c --- /dev/null +++ b/komp.jsTest.iml @@ -0,0 +1,70 @@ + + + + + + komp:commonTest + + komp.commonTest + komp.jsMain + komp.commonMain + + COMPILATION_AND_SOURCE_SET_HOLDER + + jsLegacyBrowserTest|komp:jsTest|jsLegacy + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/komp_main.iml b/komp_main.iml deleted file mode 100644 index c3b60db..0000000 --- a/komp_main.iml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - COMPILATION_AND_SOURCE_SET_HOLDER - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/komp_test.iml b/komp_test.iml deleted file mode 100644 index ccc6890..0000000 --- a/komp_test.iml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - komp_main - - COMPILATION_AND_SOURCE_SET_HOLDER - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ 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 8d1f17d..a331b87 100644 --- a/src/jsMain/kotlin/nl/astraeus/komp/DiffPatch.kt +++ b/src/jsMain/kotlin/nl/astraeus/komp/DiffPatch.kt @@ -1,11 +1,7 @@ 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.* import org.w3c.dom.events.Event -import org.w3c.dom.get const val HASH_VALUE = "komp-hash-value" @@ -34,10 +30,20 @@ object DiffPatch { fun hashesMatch(oldNode: Node, newNode: Node): Boolean { return ( oldNode is HTMLElement && - newNode is HTMLElement && - oldNode.nodeName == newNode.nodeName && - oldNode.getKompHash() == newNode.getKompHash() - ) + newNode is HTMLElement && + oldNode.nodeName == newNode.nodeName && + oldNode.getKompHash() == newNode.getKompHash() + ) + } + + private fun updateKomponentOnNode(oldNode: Node, newNode: Node) { + val komponent = newNode.asDynamic()[KOMP_KOMPONENT] as? Komponent + if (komponent != null) { + if (Komponent.logReplaceEvent) { + console.log("Keeping oldNode, set oldNode element on Komponent", oldNode, komponent) + } + komponent.element = oldNode + } } fun updateNode(oldNode: Node, newNode: Node): Node { @@ -45,6 +51,8 @@ object DiffPatch { if (Komponent.logReplaceEvent) { console.log("Hashes match", oldNode, newNode, oldNode.getKompHash(), newNode.getKompHash()) } + + updateKomponentOnNode(oldNode, newNode) return oldNode } @@ -55,6 +63,8 @@ object DiffPatch { } oldNode.textContent = newNode.textContent } + + updateKomponentOnNode(oldNode, newNode) return oldNode } @@ -73,6 +83,8 @@ object DiffPatch { } updateChildren(oldNode, newNode) oldNode.setKompHash(newNode.getKompHash()) + + updateKomponentOnNode(oldNode, newNode) return oldNode } } diff --git a/src/jsMain/kotlin/nl/astraeus/komp/HtmlBuilder.kt b/src/jsMain/kotlin/nl/astraeus/komp/HtmlBuilder.kt index d9ce14b..d4f44b4 100644 --- a/src/jsMain/kotlin/nl/astraeus/komp/HtmlBuilder.kt +++ b/src/jsMain/kotlin/nl/astraeus/komp/HtmlBuilder.kt @@ -1,13 +1,13 @@ package nl.astraeus.komp +import kotlinx.browser.document import kotlinx.html.* import org.w3c.dom.* import org.w3c.dom.css.CSSStyleDeclaration import org.w3c.dom.events.Event -import kotlin.browser.document @Suppress("NOTHING_TO_INLINE") -inline fun HTMLElement.setEvent(name: String, noinline callback: (Event) -> Unit): Unit { +inline fun HTMLElement.setEvent(name: String, noinline callback: (Event) -> Unit) { val eventName = if (name.startsWith("on")) { name.substring(2) } else { @@ -185,7 +185,7 @@ class HtmlBuilder( throw IllegalStateException("No current DOM node") } - // stupid hack as browsers doesn't support createEntityReference + // 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 }) diff --git a/src/jsMain/kotlin/nl/astraeus/komp/Komponent.kt b/src/jsMain/kotlin/nl/astraeus/komp/Komponent.kt index d9baf82..65d4a44 100644 --- a/src/jsMain/kotlin/nl/astraeus/komp/Komponent.kt +++ b/src/jsMain/kotlin/nl/astraeus/komp/Komponent.kt @@ -1,14 +1,38 @@ package nl.astraeus.komp +import kotlinx.browser.document +import kotlinx.browser.window import kotlinx.html.div import org.w3c.dom.HTMLDivElement import org.w3c.dom.HTMLElement import org.w3c.dom.Node import org.w3c.dom.css.CSSStyleDeclaration -import kotlin.browser.document +import kotlin.reflect.KProperty + +const val KOMP_KOMPONENT = "komp-komponent" typealias CssStyle = CSSStyleDeclaration.() -> Unit +class StateDelegate( + val komponent: Komponent, + initialValue: T +) { + var value: T = initialValue + + operator fun getValue(thisRef: Any?, property: KProperty<*>): T { + return value + } + + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + if (this.value?.equals(value) != true) { + this.value = value + komponent.requestUpdate() + } + } +} + +inline fun Komponent.state(initialValue: T): StateDelegate = StateDelegate(this, initialValue) + fun HtmlConsumer.include(component: Komponent) { if (Komponent.updateStrategy == UpdateStrategy.REPLACE) { if (component.element != null) { @@ -39,6 +63,9 @@ enum class UpdateStrategy { } abstract class Komponent { + private var createIndex = getNextCreateIndex() + private var dirty: Boolean = true + var element: Node? = null val declaredStyles: MutableMap = HashMap() @@ -47,13 +74,25 @@ abstract class Komponent { consumer.render() val result = consumer.finalize() + if (result.id.isBlank()) { + result.id = "komp_${createIndex}" + } + element = result + element.asDynamic()[KOMP_KOMPONENT] = this + + dirty = false return result } abstract fun HtmlBuilder.render() + fun requestUpdate() { + dirty = true + scheduleForUpdate(this) + } + open fun style(className: String, vararg imports: CssStyle, block: CssStyle = {}) { val style = (document.createElement("div") as HTMLDivElement).style for (imp in imports) { @@ -63,33 +102,41 @@ abstract class Komponent { declaredStyles[className] = style } - open fun update() = refresh() + open fun update() { + refresh() + } - open fun refresh() { + internal fun refresh() { val oldElement = element + if (logRenderEvent) { console.log("Rendering", this) } val newElement = create() if (oldElement != null) { - if (updateStrategy == UpdateStrategy.REPLACE) { + element = if (updateStrategy == UpdateStrategy.REPLACE) { if (logReplaceEvent) { console.log("Replacing", oldElement, newElement) } oldElement.parentNode?.replaceChild(newElement, oldElement) - element = newElement + newElement } else { if (logReplaceEvent) { console.log("DomDiffing", oldElement, newElement) } - element = DiffPatch.updateNode(oldElement, newElement) + DiffPatch.updateNode(oldElement, newElement) } } + + dirty = false } @JsName("remove") fun remove() { + check(updateStrategy == UpdateStrategy.REPLACE) { + "remote only works with UpdateStrategy.REPLACE" + } element?.let { val parent = it.parentElement ?: throw IllegalArgumentException("Element has no parent!?") @@ -102,6 +149,10 @@ abstract class Komponent { } companion object { + private var nextCreateIndex: Int = 1 + private var updateCallback: Int? = null + private var scheduledForUpdate = mutableSetOf() + var logRenderEvent = false var logReplaceEvent = false var updateStrategy = UpdateStrategy.DOM_DIFF @@ -115,6 +166,49 @@ abstract class Komponent { parent.appendChild(element) } } + + private fun getNextCreateIndex() = nextCreateIndex++ + + private fun scheduleForUpdate(komponent: Komponent) { + scheduledForUpdate.add(komponent) + + if (updateCallback == null) { + window.setTimeout({ + runUpdate() + }, 0) + } + } + + private fun runUpdate() { + val todo = scheduledForUpdate.sortedBy { komponent -> komponent.createIndex } + + if (logRenderEvent) { + console.log("runUpdate") + } + + todo.forEach { next -> + val element = next.element + console.log("update element", element) + if (element is HTMLElement) { + console.log("by id", document.getElementById(element.id)) + if (document.getElementById(element.id) != null) { + if (next.dirty) { + if (logRenderEvent) { + console.log("Update dirty ${next.createIndex}") + } + next.update() + } else { + if (logRenderEvent) { + console.log("Skip ${next.createIndex}") + } + } + } + } + } + + scheduledForUpdate.clear() + updateCallback = null + } } }