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 @@ } group = "nl.astraeus" -version = "1.0.1-SNAPSHOT" +version = "1.0.1" repositories { mavenCentral() @@ -18,7 +18,6 @@ testTask { useKarma { useChromiumHeadless() - //useChromeHeadless() } } } @@ -72,7 +71,7 @@ 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 @@ 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/build.gradle.kts b/build.gradle.kts index ac68b54..24868d9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ } group = "nl.astraeus" -version = "1.0.1-SNAPSHOT" +version = "1.0.1" repositories { mavenCentral() @@ -18,7 +18,6 @@ testTask { useKarma { useChromiumHeadless() - //useChromeHeadless() } } } @@ -72,7 +71,7 @@ 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 @@ 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 @@ 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 @@ 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/build.gradle.kts b/build.gradle.kts index ac68b54..24868d9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ } group = "nl.astraeus" -version = "1.0.1-SNAPSHOT" +version = "1.0.1" repositories { mavenCentral() @@ -18,7 +18,6 @@ testTask { useKarma { useChromiumHeadless() - //useChromeHeadless() } } } @@ -72,7 +71,7 @@ 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 @@ 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 @@ 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 @@ 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/build.gradle.kts b/build.gradle.kts index ac68b54..24868d9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ } group = "nl.astraeus" -version = "1.0.1-SNAPSHOT" +version = "1.0.1" repositories { mavenCentral() @@ -18,7 +18,6 @@ testTask { useKarma { useChromiumHeadless() - //useChromeHeadless() } } } @@ -72,7 +71,7 @@ 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 @@ 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 @@ 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 @@ 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/build.gradle.kts b/build.gradle.kts index ac68b54..24868d9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ } group = "nl.astraeus" -version = "1.0.1-SNAPSHOT" +version = "1.0.1" repositories { mavenCentral() @@ -18,7 +18,6 @@ testTask { useKarma { useChromiumHeadless() - //useChromeHeadless() } } } @@ -72,7 +71,7 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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/build.gradle.kts b/build.gradle.kts index ac68b54..24868d9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ } group = "nl.astraeus" -version = "1.0.1-SNAPSHOT" +version = "1.0.1" repositories { mavenCentral() @@ -18,7 +18,6 @@ testTask { useKarma { useChromiumHeadless() - //useChromeHeadless() } } } @@ -72,7 +71,7 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 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 @@ } override fun onTagEnd(tag: Tag) { + if (exceptionThrown) { + return + } + while (currentPosition.currentElement() != null) { currentPosition.currentElement()?.let { it.parentElement?.removeChild(it) @@ -237,31 +243,33 @@ 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 @@ 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 @@ 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/build.gradle.kts b/build.gradle.kts index ac68b54..24868d9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ } group = "nl.astraeus" -version = "1.0.1-SNAPSHOT" +version = "1.0.1" repositories { mavenCentral() @@ -18,7 +18,6 @@ testTask { useKarma { useChromiumHeadless() - //useChromeHeadless() } } } @@ -72,7 +71,7 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 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 @@ } override fun onTagEnd(tag: Tag) { + if (exceptionThrown) { + return + } + while (currentPosition.currentElement() != null) { currentPosition.currentElement()?.let { it.parentElement?.removeChild(it) @@ -237,31 +243,33 @@ 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 @@ 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 @@ 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 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 @@ 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 @@ */ open fun generateMemoizeHash(): Int? = null - internal fun refresh() { + private fun refresh() { val currentElement = element check(currentElement != null) { @@ -131,14 +136,19 @@ 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 @@ 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 @@ component.create(parent) } + fun setErrorHandler(handler: (KomponentException) -> Unit) { + errorHandler = handler + } + fun setUpdateInterceptor(block: (Komponent, () -> Unit) -> Unit) { interceptor = block } 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 @@ } group = "nl.astraeus" -version = "1.0.1-SNAPSHOT" +version = "1.0.1" repositories { mavenCentral() @@ -18,7 +18,6 @@ testTask { useKarma { useChromiumHeadless() - //useChromeHeadless() } } } @@ -72,7 +71,7 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 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 @@ } override fun onTagEnd(tag: Tag) { + if (exceptionThrown) { + return + } + while (currentPosition.currentElement() != null) { currentPosition.currentElement()?.let { it.parentElement?.removeChild(it) @@ -237,31 +243,33 @@ 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 @@ 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 @@ 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 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 @@ 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 @@ */ open fun generateMemoizeHash(): Int? = null - internal fun refresh() { + private fun refresh() { val currentElement = element check(currentElement != null) { @@ -131,14 +136,19 @@ 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 @@ 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 @@ 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/build.gradle.kts b/build.gradle.kts index ac68b54..24868d9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ } group = "nl.astraeus" -version = "1.0.1-SNAPSHOT" +version = "1.0.1" repositories { mavenCentral() @@ -18,7 +18,6 @@ testTask { useKarma { useChromiumHeadless() - //useChromeHeadless() } } } @@ -72,7 +71,7 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 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 @@ } override fun onTagEnd(tag: Tag) { + if (exceptionThrown) { + return + } + while (currentPosition.currentElement() != null) { currentPosition.currentElement()?.let { it.parentElement?.removeChild(it) @@ -237,31 +243,33 @@ 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 @@ 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 @@ 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 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 @@ 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 @@ */ open fun generateMemoizeHash(): Int? = null - internal fun refresh() { + private fun refresh() { val currentElement = element check(currentElement != null) { @@ -131,14 +136,19 @@ 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 @@ 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 @@ 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 @@ if (hello) { div { +"Hello" + + throw IllegalStateException("Bloe") } } else { span {