diff --git a/build.gradle.kts b/build.gradle.kts index ac68b54..24868d9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "nl.astraeus" -version = "1.0.1-SNAPSHOT" +version = "1.0.1" repositories { mavenCentral() @@ -18,7 +18,6 @@ kotlin { testTask { useKarma { useChromiumHeadless() - //useChromeHeadless() } } } @@ -72,7 +71,7 @@ publishing { maven { name = "releases" // change to point to your repo, e.g. http://my.org/repo - url = uri("https://nexus.astraeus.nl/nexus/content/repositories/releases") + setUrl("https://nexus.astraeus.nl/nexus/content/repositories/releases") credentials { val nexusUsername: String? by project val nexusPassword: String? by project @@ -84,7 +83,7 @@ publishing { maven { name = "snapshots" // change to point to your repo, e.g. http://my.org/repo - url = uri("https://nexus.astraeus.nl/nexus/content/repositories/snapshots") + setUrl("https://nexus.astraeus.nl/nexus/content/repositories/snapshots") credentials { val nexusUsername: String? by project val nexusPassword: String? by project diff --git a/docs/getting-started.md b/docs/getting-started.md index c09e6ba..09d5438 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,13 +1,14 @@ # Table of contents -* [home](home.md) -* [getting started](getting-started.md) +* [Home](home.md) +* [Getting started](getting-started.md) +* [How it works](how-it-works.md) # Getting started To get started create a new kotlin project in intellij of the type 'Browser application' -![Create 'Browser Application' project](/docs/img/create-project.png "Create 'Browser Application' project") +![Create 'Browser Application' project](/docs/img/create-project.png) Add the 'sourceSets' block with the kotlin-komponent dependency so your build.gradle.kts looks like this: @@ -43,7 +44,7 @@ kotlin { Refresh the gradle project to import the dependency. -There is now only one kt file in the project called Simple.kt, it should look like this: +There is now only one kotlin source file in the project called Simple.kt, it should look something like this: ```kotin fun main() { @@ -123,6 +124,16 @@ The TestKomponent.render method will be called to render our Komponent. As you can see events can be attached inline with the onFunction methods. The requestUpdate method will call the render method again and update the page accordingly. +After building the application you will find it in /build/distributions. + +In the index.html page you will find the following line: + +```html +
+``` + +This line is not needed for kotlin-komponent. + If you like you can use some helpers that will automatically call the requestUpdate method if the data changes, that would look like this: diff --git a/docs/home.md b/docs/home.md index 544db0e..075e1b8 100644 --- a/docs/home.md +++ b/docs/home.md @@ -1,5 +1,5 @@ # Table of contents -* [home](home.md) -* [getting started](getting-started.md) - +* [Home](home.md) +* [Getting started](getting-started.md) +* [How it works](how-it-works.md) diff --git a/docs/how-it-works.md b/docs/how-it-works.md new file mode 100644 index 0000000..0c2cf66 --- /dev/null +++ b/docs/how-it-works.md @@ -0,0 +1,23 @@ +# Table of contents + +* [Home](home.md) +* [Getting started](getting-started.md) +* [How it works](how-it-works.md) + +# How it works + +When the requestUpdate call is made to the [Komponent](src/jsMain/kotlin/nl/astraeus/komp/Komponent.kt) +the update is queued in a callback. The callback will be called after the current event is handled. + +If there are multiple updates requested, these are sorted so that the top Komponents get executed first. +This way there will not be double updates of the same komponent. + +The render call will be invoked and every html builder function (div, span etc.) will call the +different HtmlBuilder functions like onTagStart, onTagAttributeChange etc. + +In these functions the HtmlBuilder will compare the dom against the call being made and it will update the DOM +if needed. + + + + diff --git a/src/jsMain/kotlin/nl/astraeus/komp/ElementExtentions.kt b/src/jsMain/kotlin/nl/astraeus/komp/ElementExtentions.kt index 1fd0c4d..6fc132c 100644 --- a/src/jsMain/kotlin/nl/astraeus/komp/ElementExtentions.kt +++ b/src/jsMain/kotlin/nl/astraeus/komp/ElementExtentions.kt @@ -152,3 +152,15 @@ internal fun Element.getKompEvents(): MutableMap Unit> { return this.asDynamic()["komp-events"] ?: mutableMapOf() } +internal fun Element.findElementIndex(): Int { + val childNodes = parentElement?.children + if (childNodes != null) { + for (index in 0 until childNodes.length) { + if (childNodes[index] == this) { + return index + } + } + } + + return 0 +} \ No newline at end of file diff --git a/src/jsMain/kotlin/nl/astraeus/komp/HtmlBuilder.kt b/src/jsMain/kotlin/nl/astraeus/komp/HtmlBuilder.kt index 2f8278e..4a80524 100644 --- a/src/jsMain/kotlin/nl/astraeus/komp/HtmlBuilder.kt +++ b/src/jsMain/kotlin/nl/astraeus/komp/HtmlBuilder.kt @@ -73,11 +73,13 @@ private fun ArrayList.replace(new: Node) { private fun Node.asElement() = this as? HTMLElement class HtmlBuilder( + val komponent: Komponent?, parent: Element, - childIndex: Int = 0 + childIndex: Int = 0, ) : HtmlConsumer { private var currentPosition = arrayListOf() private var inDebug = false + private var exceptionThrown = false var currentNode: Node? = null var root: Element? = null @@ -225,6 +227,10 @@ class HtmlBuilder( } override fun onTagEnd(tag: Tag) { + if (exceptionThrown) { + return + } + while (currentPosition.currentElement() != null) { currentPosition.currentElement()?.let { it.parentElement?.removeChild(it) @@ -237,31 +243,33 @@ class HtmlBuilder( currentPosition.pop() - val setAttrs: List = currentElement.asDynamic()["komp-attributes"] ?: listOf() + if (currentElement != null) { + val setAttrs: List = currentElement?.asDynamic()["komp-attributes"] ?: listOf() - // remove attributes that where not set - val element = currentElement - if (element?.hasAttributes() == true) { - for (index in 0 until element.attributes.length) { - val attr = element.attributes[index] - if (attr != null) { + // remove attributes that where not set + val element = currentElement + if (element?.hasAttributes() == true) { + for (index in 0 until element.attributes.length) { + val attr = element.attributes[index] + if (attr != null) { - if (element is HTMLElement && attr.name == "data-has-focus" && "true" == attr.value) { - element.focus() - } + if (element is HTMLElement && attr.name == "data-has-focus" && "true" == attr.value) { + element.focus() + } - if (attr.name != "style" && !setAttrs.contains(attr.name)) { - if (element is HTMLInputElement) { - if (attr.name == "checkbox") { - element.checked = false - } else if (attr.name == "value") { - element.value = "" + if (attr.name != "style" && !setAttrs.contains(attr.name)) { + if (element is HTMLInputElement) { + if (attr.name == "checkbox") { + element.checked = false + } else if (attr.name == "value") { + element.value = "" + } + } else { + if (Komponent.logReplaceEvent) { + console.log("Clear attribute [${attr.name}] on $element)") + } + element.removeAttribute(attr.name) } - } else { - if (Komponent.logReplaceEvent) { - console.log("Clear attribute [${attr.name}] on $element)") - } - element.removeAttribute(attr.name) } } } @@ -367,6 +375,43 @@ class HtmlBuilder( currentPosition.nextElement() } + override fun onTagError(tag: Tag, exception: Throwable) { + exceptionThrown = true + + if (exception !is KomponentException) { + val position = mutableListOf() + var ce = currentElement + while(ce != null) { + position.add(ce) + ce = ce.parentElement + } + val builder = StringBuilder() + for (element in position.reversed()) { + builder.append("> ") + builder.append(element.tagName) + builder.append("[") + builder.append(element.findElementIndex()) + builder.append("]") + if (element.hasAttribute("class")) { + builder.append("(") + builder.append(element.getAttribute("class")) + builder.append(")") + } + builder.append(" ") + } + throw KomponentException( + komponent, + currentElement, + tag, + builder.toString(), + exception.message ?: "error", + exception + ) + } else { + throw exception + } + } + override fun finalize(): Element { //logReplace"finalize, currentPosition: $currentPosition") return root ?: throw IllegalStateException("We can't finalize as there was no tags") @@ -375,7 +420,7 @@ class HtmlBuilder( companion object { fun create(content: HtmlBuilder.() -> Unit): Element { val container = document.createElement("div") as HTMLElement - val consumer = HtmlBuilder(container, 0) + val consumer = HtmlBuilder(null, container, 0) content.invoke(consumer) return consumer.root ?: error("No root element found after render!") } diff --git a/src/jsMain/kotlin/nl/astraeus/komp/Komponent.kt b/src/jsMain/kotlin/nl/astraeus/komp/Komponent.kt index 3cf03a9..428aa88 100644 --- a/src/jsMain/kotlin/nl/astraeus/komp/Komponent.kt +++ b/src/jsMain/kotlin/nl/astraeus/komp/Komponent.kt @@ -5,7 +5,6 @@ import kotlinx.html.FlowOrMetaDataOrPhrasingContent import org.w3c.dom.Element import org.w3c.dom.HTMLElement import org.w3c.dom.get -import kotlin.reflect.KProperty private var currentKomponent: Komponent? = null fun FlowOrMetaDataOrPhrasingContent.currentKomponent(): Komponent = @@ -38,13 +37,19 @@ abstract class Komponent { open fun create(parent: Element, childIndex: Int? = null) { onBeforeUpdate() val builder = HtmlBuilder( + this, parent, childIndex ?: parent.childNodes.length ) - currentKomponent = this - builder.render() - currentKomponent = null + try { + currentKomponent = this + builder.render() + } catch(e: KomponentException) { + errorHandler(e) + } finally { + currentKomponent = null + } element = builder.root updateMemoizeHash() @@ -117,7 +122,7 @@ abstract class Komponent { */ open fun generateMemoizeHash(): Int? = null - internal fun refresh() { + private fun refresh() { val currentElement = element check(currentElement != null) { @@ -131,14 +136,19 @@ abstract class Komponent { childIndex = index } } - val consumer = HtmlBuilder(parent, childIndex) - consumer.root = null + val builder = HtmlBuilder(this, parent, childIndex) + builder.root = null - currentKomponent = this - consumer.render() - currentKomponent = null + try { + currentKomponent = this + builder.render() + } catch(e: KomponentException) { + errorHandler(e) + } finally { + currentKomponent = null + } - element = consumer.root + element = builder.root dirty = false } @@ -149,6 +159,18 @@ abstract class Komponent { companion object { private var nextCreateIndex: Int = 1 private var updateCallback: Int? = null + private var errorHandler: (KomponentException) -> Unit = { ke -> + console.error("Render error in Komponent", ke) + + ke.element?.innerHTML = """
Render error!
""" + + window.alert(""" + Error in Komponent '${ke.komponent}', ${ke.message} + Tag: ${ke.tag.tagName} + See console log for details + Position: ${ke.position}""".trimIndent() + ) + } private var scheduledForUpdate = mutableSetOf() private var interceptor: (Komponent, () -> Unit) -> Unit = { _, block -> block() } @@ -161,6 +183,10 @@ abstract class Komponent { component.create(parent) } + fun setErrorHandler(handler: (KomponentException) -> Unit) { + errorHandler = handler + } + fun setUpdateInterceptor(block: (Komponent, () -> Unit) -> Unit) { interceptor = block } diff --git a/src/jsMain/kotlin/nl/astraeus/komp/KomponentException.kt b/src/jsMain/kotlin/nl/astraeus/komp/KomponentException.kt new file mode 100644 index 0000000..a2bc763 --- /dev/null +++ b/src/jsMain/kotlin/nl/astraeus/komp/KomponentException.kt @@ -0,0 +1,18 @@ +package nl.astraeus.komp + +import kotlinx.html.Tag +import org.w3c.dom.Element + +class KomponentException( + val komponent: Komponent?, + val element: Element?, + val tag: Tag, + val position: String, + message: String, + cause: Throwable +) : RuntimeException(message, cause) { + + override fun toString(): String { + return "KompException(message='$message', tag='$tag', position='$position')" + } +} diff --git a/src/jsTest/kotlin/nl/astraeus/komp/TestUpdate.kt b/src/jsTest/kotlin/nl/astraeus/komp/TestUpdate.kt index a6eb0e9..4deaa0f 100644 --- a/src/jsTest/kotlin/nl/astraeus/komp/TestUpdate.kt +++ b/src/jsTest/kotlin/nl/astraeus/komp/TestUpdate.kt @@ -61,6 +61,8 @@ class SimpleKomponent : Komponent() { if (hello) { div { +"Hello" + + throw IllegalStateException("Bloe") } } else { span {