v. 1.0.1 - Added error handler and default error handling.

Took 1 hour 47 minutes
This commit is contained in:
2022-02-01 12:02:19 +01:00
parent 88625e65a4
commit f82ef1da70
9 changed files with 181 additions and 45 deletions

View File

@@ -6,7 +6,7 @@ plugins {
} }
group = "nl.astraeus" group = "nl.astraeus"
version = "1.0.1-SNAPSHOT" version = "1.0.1"
repositories { repositories {
mavenCentral() mavenCentral()
@@ -18,7 +18,6 @@ kotlin {
testTask { testTask {
useKarma { useKarma {
useChromiumHeadless() useChromiumHeadless()
//useChromeHeadless()
} }
} }
} }
@@ -72,7 +71,7 @@ publishing {
maven { maven {
name = "releases" name = "releases"
// change to point to your repo, e.g. http://my.org/repo // 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 { credentials {
val nexusUsername: String? by project val nexusUsername: String? by project
val nexusPassword: String? by project val nexusPassword: String? by project
@@ -84,7 +83,7 @@ publishing {
maven { maven {
name = "snapshots" name = "snapshots"
// change to point to your repo, e.g. http://my.org/repo // 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 { credentials {
val nexusUsername: String? by project val nexusUsername: String? by project
val nexusPassword: String? by project val nexusPassword: String? by project

View File

@@ -1,13 +1,14 @@
# Table of contents # Table of contents
* [home](home.md) * [Home](home.md)
* [getting started](getting-started.md) * [Getting started](getting-started.md)
* [How it works](how-it-works.md)
# Getting started # Getting started
To get started create a new kotlin project in intellij of the type 'Browser application' 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: 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. 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 ```kotin
fun main() { 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 on<event>Function methods. As you can see events can be attached inline with the on<event>Function methods.
The requestUpdate method will call the render method again and update the page accordingly. 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
<div id="root"></div>
```
This line is not needed for kotlin-komponent.
If you like you can use some helpers that will automatically call the requestUpdate method if If you like you can use some helpers that will automatically call the requestUpdate method if
the data changes, that would look like this: the data changes, that would look like this:

View File

@@ -1,5 +1,5 @@
# Table of contents # Table of contents
* [home](home.md) * [Home](home.md)
* [getting started](getting-started.md) * [Getting started](getting-started.md)
* [How it works](how-it-works.md)

23
docs/how-it-works.md Normal file
View File

@@ -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.

View File

@@ -152,3 +152,15 @@ internal fun Element.getKompEvents(): MutableMap<String, (Event) -> Unit> {
return this.asDynamic()["komp-events"] ?: mutableMapOf() 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
}

View File

@@ -73,11 +73,13 @@ private fun ArrayList<ElementIndex>.replace(new: Node) {
private fun Node.asElement() = this as? HTMLElement private fun Node.asElement() = this as? HTMLElement
class HtmlBuilder( class HtmlBuilder(
val komponent: Komponent?,
parent: Element, parent: Element,
childIndex: Int = 0 childIndex: Int = 0,
) : HtmlConsumer { ) : HtmlConsumer {
private var currentPosition = arrayListOf<ElementIndex>() private var currentPosition = arrayListOf<ElementIndex>()
private var inDebug = false private var inDebug = false
private var exceptionThrown = false
var currentNode: Node? = null var currentNode: Node? = null
var root: Element? = null var root: Element? = null
@@ -225,6 +227,10 @@ class HtmlBuilder(
} }
override fun onTagEnd(tag: Tag) { override fun onTagEnd(tag: Tag) {
if (exceptionThrown) {
return
}
while (currentPosition.currentElement() != null) { while (currentPosition.currentElement() != null) {
currentPosition.currentElement()?.let { currentPosition.currentElement()?.let {
it.parentElement?.removeChild(it) it.parentElement?.removeChild(it)
@@ -237,31 +243,33 @@ class HtmlBuilder(
currentPosition.pop() currentPosition.pop()
val setAttrs: List<String> = currentElement.asDynamic()["komp-attributes"] ?: listOf() if (currentElement != null) {
val setAttrs: List<String> = currentElement?.asDynamic()["komp-attributes"] ?: listOf()
// remove attributes that where not set // remove attributes that where not set
val element = currentElement val element = currentElement
if (element?.hasAttributes() == true) { if (element?.hasAttributes() == true) {
for (index in 0 until element.attributes.length) { for (index in 0 until element.attributes.length) {
val attr = element.attributes[index] val attr = element.attributes[index]
if (attr != null) { if (attr != null) {
if (element is HTMLElement && attr.name == "data-has-focus" && "true" == attr.value) { if (element is HTMLElement && attr.name == "data-has-focus" && "true" == attr.value) {
element.focus() element.focus()
} }
if (attr.name != "style" && !setAttrs.contains(attr.name)) { if (attr.name != "style" && !setAttrs.contains(attr.name)) {
if (element is HTMLInputElement) { if (element is HTMLInputElement) {
if (attr.name == "checkbox") { if (attr.name == "checkbox") {
element.checked = false element.checked = false
} else if (attr.name == "value") { } else if (attr.name == "value") {
element.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() currentPosition.nextElement()
} }
override fun onTagError(tag: Tag, exception: Throwable) {
exceptionThrown = true
if (exception !is KomponentException) {
val position = mutableListOf<Element>()
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 { override fun finalize(): Element {
//logReplace"finalize, currentPosition: $currentPosition") //logReplace"finalize, currentPosition: $currentPosition")
return root ?: throw IllegalStateException("We can't finalize as there was no tags") return root ?: throw IllegalStateException("We can't finalize as there was no tags")
@@ -375,7 +420,7 @@ class HtmlBuilder(
companion object { companion object {
fun create(content: HtmlBuilder.() -> Unit): Element { fun create(content: HtmlBuilder.() -> Unit): Element {
val container = document.createElement("div") as HTMLElement val container = document.createElement("div") as HTMLElement
val consumer = HtmlBuilder(container, 0) val consumer = HtmlBuilder(null, container, 0)
content.invoke(consumer) content.invoke(consumer)
return consumer.root ?: error("No root element found after render!") return consumer.root ?: error("No root element found after render!")
} }

View File

@@ -5,7 +5,6 @@ import kotlinx.html.FlowOrMetaDataOrPhrasingContent
import org.w3c.dom.Element import org.w3c.dom.Element
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import org.w3c.dom.get import org.w3c.dom.get
import kotlin.reflect.KProperty
private var currentKomponent: Komponent? = null private var currentKomponent: Komponent? = null
fun FlowOrMetaDataOrPhrasingContent.currentKomponent(): Komponent = fun FlowOrMetaDataOrPhrasingContent.currentKomponent(): Komponent =
@@ -38,13 +37,19 @@ abstract class Komponent {
open fun create(parent: Element, childIndex: Int? = null) { open fun create(parent: Element, childIndex: Int? = null) {
onBeforeUpdate() onBeforeUpdate()
val builder = HtmlBuilder( val builder = HtmlBuilder(
this,
parent, parent,
childIndex ?: parent.childNodes.length childIndex ?: parent.childNodes.length
) )
currentKomponent = this try {
builder.render() currentKomponent = this
currentKomponent = null builder.render()
} catch(e: KomponentException) {
errorHandler(e)
} finally {
currentKomponent = null
}
element = builder.root element = builder.root
updateMemoizeHash() updateMemoizeHash()
@@ -117,7 +122,7 @@ abstract class Komponent {
*/ */
open fun generateMemoizeHash(): Int? = null open fun generateMemoizeHash(): Int? = null
internal fun refresh() { private fun refresh() {
val currentElement = element val currentElement = element
check(currentElement != null) { check(currentElement != null) {
@@ -131,14 +136,19 @@ abstract class Komponent {
childIndex = index childIndex = index
} }
} }
val consumer = HtmlBuilder(parent, childIndex) val builder = HtmlBuilder(this, parent, childIndex)
consumer.root = null builder.root = null
currentKomponent = this try {
consumer.render() currentKomponent = this
currentKomponent = null builder.render()
} catch(e: KomponentException) {
errorHandler(e)
} finally {
currentKomponent = null
}
element = consumer.root element = builder.root
dirty = false dirty = false
} }
@@ -149,6 +159,18 @@ abstract class Komponent {
companion object { companion object {
private var nextCreateIndex: Int = 1 private var nextCreateIndex: Int = 1
private var updateCallback: Int? = null private var updateCallback: Int? = null
private var errorHandler: (KomponentException) -> Unit = { ke ->
console.error("Render error in Komponent", ke)
ke.element?.innerHTML = """<div class="komponent-error">Render error!</div>"""
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<Komponent>() private var scheduledForUpdate = mutableSetOf<Komponent>()
private var interceptor: (Komponent, () -> Unit) -> Unit = { _, block -> block() } private var interceptor: (Komponent, () -> Unit) -> Unit = { _, block -> block() }
@@ -161,6 +183,10 @@ abstract class Komponent {
component.create(parent) component.create(parent)
} }
fun setErrorHandler(handler: (KomponentException) -> Unit) {
errorHandler = handler
}
fun setUpdateInterceptor(block: (Komponent, () -> Unit) -> Unit) { fun setUpdateInterceptor(block: (Komponent, () -> Unit) -> Unit) {
interceptor = block interceptor = block
} }

View File

@@ -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')"
}
}

View File

@@ -61,6 +61,8 @@ class SimpleKomponent : Komponent() {
if (hello) { if (hello) {
div { div {
+"Hello" +"Hello"
throw IllegalStateException("Bloe")
} }
} else { } else {
span { span {