diff --git a/build.gradle.kts b/build.gradle.kts index 4f0b9fc..2aebb27 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -68,6 +68,7 @@ val javadocJar by tasks.registering(Jar::class) { publishing { repositories { + mavenLocal() maven { name = "releases" // change to point to your repo, e.g. http://my.org/repo diff --git a/src/jsMain/kotlin/nl/astraeus/komp/ElementExtentions.kt b/src/jsMain/kotlin/nl/astraeus/komp/ElementExtentions.kt new file mode 100644 index 0000000..1fd0c4d --- /dev/null +++ b/src/jsMain/kotlin/nl/astraeus/komp/ElementExtentions.kt @@ -0,0 +1,154 @@ +package nl.astraeus.komp + +import org.w3c.dom.Element +import org.w3c.dom.HTMLInputElement +import org.w3c.dom.events.Event +import org.w3c.dom.get + +private fun Int.asSpaces(): String { + val result = StringBuilder() + + repeat(this) { + result.append(" ") + } + return result.toString() +} + +fun Element.printTree(indent: Int = 0): String { + val result = StringBuilder() + + result.append(indent.asSpaces()) + result.append(tagName) + if (this.namespaceURI != "http://www.w3.org/1999/xhtml") { + result.append(" [") + result.append(namespaceURI) + result.append("]") + } + result.append(" (") + var first = true + if (hasAttributes()) { + for (index in 0 until attributes.length) { + if (!first) { + result.append(", ") + } else { + first = false + } + result.append(attributes[index]?.localName) + result.append("=") + result.append(attributes[index]?.value) + } + } + 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) { + result.append(it.printTree(indent + 2)) + } else { + result.append((indent + 2).asSpaces()) + result.append(it.textContent) + result.append("\n") + } + } + } + result.append(indent.asSpaces()) + result.append("}\n") + + return result.toString() +} + +internal fun Element.clearKompAttributes() { + val attributes = this.asDynamic()["komp-attributes"] as MutableSet? + + if (attributes == null) { + this.asDynamic()["komp-attributes"] = mutableSetOf() + } else { + attributes.clear() + } + + if (this is HTMLInputElement) { + this.checked = false + } +} + +internal fun Element.getKompAttributes(): MutableSet { + var result: MutableSet? = this.asDynamic()["komp-attributes"] as MutableSet? + + if (result == null) { + result = mutableSetOf() + + this.asDynamic()["komp-attributes"] = result + } + + return result +} + +internal fun Element.setKompAttribute(name: String, value: String) { + val setAttrs: MutableSet = getKompAttributes() + setAttrs.add(name) + + if (this is HTMLInputElement) { + when (name) { + "checked" -> { + this.checked = value == "checked" + } + "value" -> { + this.value = value + + } + else -> { + setAttribute(name, value) + } + } + } else if (this.getAttribute(name) != value) { + setAttribute(name, value) + } +} + +internal fun Element.clearKompEvents() { + for ((name, event) in getKompEvents()) { + removeEventListener(name, event) + } + + val events = this.asDynamic()["komp-events"] as MutableMap Unit>? + + if (events == null) { + this.asDynamic()["komp-events"] = mutableMapOf Unit>() + } else { + events.clear() + } +} + +internal fun Element.setKompEvent(name: String, event: (Event) -> Unit) { + val eventName: String = if (name.startsWith("on")) { + name.substring(2) + } else { + name + } + + val events: MutableMap Unit> = getKompEvents() + + events[eventName]?.let { + println("Warn event '$eventName' already defined!") + removeEventListener(eventName, it) + } + + events[eventName] = event + + this.asDynamic()["komp-events"] = events + + this.addEventListener(eventName, event) +} + +internal fun Element.getKompEvents(): MutableMap Unit> { + return this.asDynamic()["komp-events"] ?: mutableMapOf() +} + diff --git a/src/jsMain/kotlin/nl/astraeus/komp/HtmlBuilder.kt b/src/jsMain/kotlin/nl/astraeus/komp/HtmlBuilder.kt index 401cb4e..2f8278e 100644 --- a/src/jsMain/kotlin/nl/astraeus/komp/HtmlBuilder.kt +++ b/src/jsMain/kotlin/nl/astraeus/komp/HtmlBuilder.kt @@ -24,155 +24,9 @@ interface HtmlConsumer : TagConsumer { fun debug(block: HtmlConsumer.() -> Unit) } -fun Int.asSpaces(): String { - val result = StringBuilder() - repeat(this) { - result.append(" ") - } - return result.toString() -} - fun FlowOrMetaDataOrPhrasingContent.currentElement(): Element = currentElement ?: error("No current element defined!") -fun Element.printTree(indent: Int = 0): String { - val result = StringBuilder() - - result.append(indent.asSpaces()) - result.append(tagName) - if (this.namespaceURI != "http://www.w3.org/1999/xhtml") { - result.append(" [") - result.append(namespaceURI) - result.append("]") - } - result.append(" (") - var first = true - if (hasAttributes()) { - for (index in 0 until attributes.length) { - if (!first) { - result.append(", ") - } else { - first = false - } - result.append(attributes[index]?.localName) - result.append("=") - result.append(attributes[index]?.value) - } - } - 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) { - result.append(it.printTree(indent + 2)) - } else { - result.append((indent + 2).asSpaces()) - result.append(it.textContent) - result.append("\n") - } - } - } - result.append(indent.asSpaces()) - result.append("}\n") - - return result.toString() -} - -private fun Element.clearKompAttributes() { - val attributes = this.asDynamic()["komp-attributes"] as MutableSet? - - if (attributes == null) { - this.asDynamic()["komp-attributes"] = mutableSetOf() - } else { - attributes.clear() - } - - if (this is HTMLInputElement) { - this.checked = false - } -} - -private fun Element.getKompAttributes(): MutableSet { - var result: MutableSet? = this.asDynamic()["komp-attributes"] as MutableSet? - - if (result == null) { - result = mutableSetOf() - - this.asDynamic()["komp-attributes"] = result - } - - return result -} - -fun Element.setKompAttribute(name: String, value: String) { - val setAttrs: MutableSet = getKompAttributes() - setAttrs.add(name) - - if (this is HTMLInputElement) { - when (name) { - "checked" -> { - this.checked = value == "checked" - } - "value" -> { - this.value = value - - } - else -> { - setAttribute(name, value) - } - } - } else if (this.getAttribute(name) != value) { - setAttribute(name, value) - } -} - -private fun Element.clearKompEvents() { - for ((name, event) in getKompEvents()) { - currentElement?.removeEventListener(name, event) - } - - val events = this.asDynamic()["komp-events"] as MutableMap Unit>? - - if (events == null) { - this.asDynamic()["komp-events"] = mutableMapOf Unit>() - } else { - events.clear() - } -} - -private fun Element.setKompEvent(name: String, event: (Event) -> Unit) { - val eventName: String = if (name.startsWith("on")) { - name.substring(2) - } else { - name - } - - val events: MutableMap Unit> = getKompEvents() - - events[eventName]?.let { - println("Warn event already defined!") - currentElement?.removeEventListener(eventName, it) - } - - events[eventName] = event - - this.asDynamic()["komp-events"] = events - - this.addEventListener(eventName, event) -} - -private fun Element.getKompEvents(): MutableMap Unit> { - return this.asDynamic()["komp-events"] ?: mutableMapOf() -} - private data class ElementIndex( val parent: Node, var childIndex: Int @@ -219,14 +73,13 @@ private fun ArrayList.replace(new: Node) { private fun Node.asElement() = this as? HTMLElement class HtmlBuilder( - val parent: Element, - var childIndex: Int = 0 + parent: Element, + childIndex: Int = 0 ) : HtmlConsumer { private var currentPosition = arrayListOf() private var inDebug = false var currentNode: Node? = null var root: Element? = null - val currentAttributes: MutableMap = mutableMapOf() init { currentPosition.add(ElementIndex(parent, childIndex)) @@ -256,16 +109,19 @@ class HtmlBuilder( } override fun debug(block: HtmlConsumer.() -> Unit) { + val enableAssertions = Komponent.enableAssertions + Komponent.enableAssertions = true inDebug = true try { block.invoke(this) } finally { inDebug = false + Komponent.enableAssertions = enableAssertions } } - fun logReplace(msg: String) { + private fun logReplace(msg: String) { if (Komponent.logReplaceEvent && inDebug) { console.log(msg) } @@ -347,7 +203,9 @@ class HtmlBuilder( override fun onTagAttributeChange(tag: Tag, attribute: String, value: String?) { logReplace("onTagAttributeChange, ${tag.tagName} [$attribute, $value]") - checkTag(tag) + if (Komponent.enableAssertions) { + checkTag(tag) + } if (value == null) { currentElement?.removeAttribute(attribute.lowercase()) @@ -357,9 +215,11 @@ class HtmlBuilder( } override fun onTagEvent(tag: Tag, event: String, value: (Event) -> Unit) { - //logReplace"onTagEvent, ${tag.tagName} [$event, $value]") + logReplace("onTagEvent, ${tag.tagName} [$event, $value]") - checkTag(tag) + if (Komponent.enableAssertions) { + checkTag(tag) + } currentElement?.setKompEvent(event.lowercase(), value) } @@ -371,7 +231,9 @@ class HtmlBuilder( } } - checkTag(tag) + if (Komponent.enableAssertions) { + checkTag(tag) + } currentPosition.pop() diff --git a/src/jsMain/kotlin/nl/astraeus/komp/Komponent.kt b/src/jsMain/kotlin/nl/astraeus/komp/Komponent.kt index 0cf806c..3cf03a9 100644 --- a/src/jsMain/kotlin/nl/astraeus/komp/Komponent.kt +++ b/src/jsMain/kotlin/nl/astraeus/komp/Komponent.kt @@ -1,31 +1,15 @@ package nl.astraeus.komp import kotlinx.browser.window +import kotlinx.html.FlowOrMetaDataOrPhrasingContent import org.w3c.dom.Element import org.w3c.dom.HTMLElement -import org.w3c.dom.Node import org.w3c.dom.get import kotlin.reflect.KProperty -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) +private var currentKomponent: Komponent? = null +fun FlowOrMetaDataOrPhrasingContent.currentKomponent(): Komponent = + currentKomponent ?: error("No current komponent defined! Only call from render code!") enum class UnsafeMode { UNSAFE_ALLOWED, @@ -33,12 +17,23 @@ enum class UnsafeMode { UNSAFE_SVG_ONLY } +var Element.memoizeHash: String? + get() { + return getAttribute("memoize-hash") + } + set(value) { + if (value != null) { + setAttribute("memoize-hash", value.toString()) + } else { + removeAttribute("memoize-hash") + } + } + abstract class Komponent { val createIndex = getNextCreateIndex() private var dirty: Boolean = true - private var lastMemoizeHash: Int? = null - var element: Node? = null + var element: Element? = null open fun create(parent: Element, childIndex: Int? = null) { onBeforeUpdate() @@ -47,13 +42,30 @@ abstract class Komponent { childIndex ?: parent.childNodes.length ) + currentKomponent = this builder.render() + currentKomponent = null + element = builder.root - lastMemoizeHash = generateMemoizeHash() + updateMemoizeHash() onAfterUpdate() } - fun memoizeChanged() = lastMemoizeHash == null || lastMemoizeHash != generateMemoizeHash() + fun memoizeChanged() = element?.memoizeHash == null || element?.memoizeHash != fullMemoizeHash() + + fun updateMemoizeHash() { + element?.memoizeHash = fullMemoizeHash() + } + + private fun fullMemoizeHash(): String? { + val generated = generateMemoizeHash() + + return if (generated != null) { + "${this::class.simpleName}:${generateMemoizeHash()}" + } else { + null + } + } abstract fun HtmlBuilder.render() @@ -121,7 +133,11 @@ abstract class Komponent { } val consumer = HtmlBuilder(parent, childIndex) consumer.root = null + + currentKomponent = this consumer.render() + currentKomponent = null + element = consumer.root dirty = false } @@ -138,6 +154,7 @@ abstract class Komponent { var logRenderEvent = false var logReplaceEvent = false + var enableAssertions = false var unsafeMode = UnsafeMode.UNSAFE_DISABLED fun create(parent: HTMLElement, component: Komponent) { @@ -183,10 +200,10 @@ abstract class Komponent { } val memoizeHash = next.generateMemoizeHash() - if (memoizeHash == null || next.lastMemoizeHash != memoizeHash) { + if (next.memoizeChanged()) { next.onBeforeUpdate() next.update() - next.lastMemoizeHash = memoizeHash + next.updateMemoizeHash() next.onAfterUpdate() } else if (logRenderEvent) { console.log("Skipped render, memoizeHash is equal $next-[$memoizeHash]") diff --git a/src/jsMain/kotlin/nl/astraeus/komp/State.kt b/src/jsMain/kotlin/nl/astraeus/komp/State.kt new file mode 100644 index 0000000..37e0ee5 --- /dev/null +++ b/src/jsMain/kotlin/nl/astraeus/komp/State.kt @@ -0,0 +1,35 @@ +package nl.astraeus.komp + +import kotlin.reflect.KProperty + +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 +)