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"
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

View File

@@ -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 on<event>Function 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
<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
the data changes, that would look like this:

View File

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

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()
}
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
class HtmlBuilder(
val komponent: Komponent?,
parent: Element,
childIndex: Int = 0
childIndex: Int = 0,
) : HtmlConsumer {
private var currentPosition = arrayListOf<ElementIndex>()
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<String> = currentElement.asDynamic()["komp-attributes"] ?: listOf()
if (currentElement != null) {
val setAttrs: List<String> = 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<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 {
//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!")
}

View File

@@ -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 = """<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 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
}

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) {
div {
+"Hello"
throw IllegalStateException("Bloe")
}
} else {
span {