1 Commits

Author SHA1 Message Date
5d20a30ab4 v. 1.0.3 - Simplify rendering, replace i.o. update
Took 1 hour 27 minutes
2022-02-07 14:34:41 +01:00
15 changed files with 151 additions and 344 deletions

1
.gitignore vendored
View File

@@ -8,4 +8,3 @@ local.properties
*.ipr
*.iws
kotlin-js-store
.idea

View File

@@ -1,19 +1,19 @@
plugins {
kotlin("multiplatform") version "1.9.23"
id("maven-publish")
id("signing")
kotlin("multiplatform") version "1.6.10"
`maven-publish`
signing
id("org.jetbrains.dokka") version "1.5.31"
}
group = "nl.astraeus"
version = "1.2.3-SNAPSHOT"
version = "1.0.3"
repositories {
mavenCentral()
}
kotlin {
js(IR) {
js(BOTH) {
browser {
testTask {
useKarma {
@@ -22,37 +22,20 @@ kotlin {
}
}
}
/*
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
//moduleName = project.name
browser()
mavenPublication {
groupId = group as String
pom { name = "${project.name}-wasm-js" }
}
}
@OptIn(ExperimentalKotlinGradlePluginApi::class)
applyDefaultHierarchyTemplate {
common {
group("jsCommon") {
withJs()
// TODO: switch to `withWasmJs()` after upgrade to Kotlin 2.0
withWasm()
}
}
}
*/
sourceSets {
val commonMain by getting {
dependencies {
api("org.jetbrains.kotlinx:kotlinx-html:0.11.0")
implementation(kotlin("stdlib-common"))
api("org.jetbrains.kotlinx:kotlinx-html-js:0.7.3")
}
}
val jsMain by getting {
dependencies {
implementation(kotlin("stdlib-js"))
}
}
val jsMain by getting
val jsTest by getting {
dependencies {
implementation(kotlin("test-js"))
@@ -88,25 +71,25 @@ publishing {
maven {
name = "releases"
// change to point to your repo, e.g. http://my.org/repo
setUrl("https://reposilite.astraeus.nl/releases")
setUrl("https://nexus.astraeus.nl/nexus/content/repositories/releases")
credentials {
val reposiliteUsername: String? by project
val reposilitePassword: String? by project
val nexusUsername: String? by project
val nexusPassword: String? by project
username = reposiliteUsername
password = reposilitePassword
username = nexusUsername
password = nexusPassword
}
}
maven {
name = "snapshots"
// change to point to your repo, e.g. http://my.org/repo
setUrl("https://reposilite.astraeus.nl/snapshots")
setUrl("https://nexus.astraeus.nl/nexus/content/repositories/snapshots")
credentials {
val reposiliteUsername: String? by project
val reposilitePassword: String? by project
val nexusUsername: String? by project
val nexusPassword: String? by project
username = reposiliteUsername
password = reposilitePassword
username = nexusUsername
password = nexusPassword
}
}
maven {
@@ -153,23 +136,3 @@ publishing {
signing {
sign(publishing.publications)
}
tasks.named<Task>("signJsPublication") {
dependsOn(tasks.named<Task>("publishKotlinMultiplatformPublicationToMavenLocal"))
}
tasks.named<Task>("publishJsPublicationToReleasesRepository") {
dependsOn(tasks.named<Task>("signKotlinMultiplatformPublication"))
}
tasks.named<Task>("publishKotlinMultiplatformPublicationToMavenLocalRepository") {
dependsOn(tasks.named<Task>("signJsPublication"))
}
tasks.named<Task>("publishKotlinMultiplatformPublicationToReleasesRepository") {
dependsOn(tasks.named<Task>("signJsPublication"))
}
tasks.named<Task>("publishKotlinMultiplatformPublicationToSonatypeRepository") {
dependsOn(tasks.named<Task>("signJsPublication"))
}

View File

@@ -56,7 +56,7 @@ fun greet() = "world"
Replace the code in the file with the following for a simple click app:
```kotlin
```koltin
import kotlinx.browser.document
import kotlinx.html.button
import kotlinx.html.div
@@ -144,4 +144,4 @@ the data changes, that would look like this:
In that case you can remove the requestUpdate call from the onClickFunction.
You can find a working repository of this example here: [kotlin-komponent-start](https://github.com/rnentjes/kotlin-komponent-start)
You can find a working repository of this example here: [example]()

View File

@@ -3,4 +3,3 @@
* [Home](home.md)
* [Getting started](getting-started.md)
* [How it works](how-it-works.md)

View File

@@ -15,8 +15,8 @@ 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
as needed.
In these functions the HtmlBuilder will compare the dom against the call being made and it will update the DOM
if needed.

View File

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 184 KiB

View File

@@ -1,5 +1,5 @@
#Wed Mar 04 13:29:12 CET 2020
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStorePath=wrapper/dists

View File

@@ -9,5 +9,3 @@ See the komp-todo repository for a basic example here: [komp-todo](https://githu
For a more complete example take a look at the simple-password-manager repository: [simple-password-manager](https://github.com/rnentjes/simple-password-manager)
Available on maven central: "nl.astraeus:kotlin-komponent-js:1.0.0"
Some getting started documentation can be found [here](docs/getting-started.md)

View File

@@ -1,9 +1,8 @@
package nl.astraeus.komp
import org.w3c.dom.events.Event
import org.w3c.dom.Element
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.events.EventListener
import org.w3c.dom.events.Event
import org.w3c.dom.get
private fun Int.asSpaces(): String {
@@ -41,14 +40,6 @@ fun Element.printTree(indent: Int = 0): String {
}
result.append(") {")
result.append("\n")
for ((name, event) in getKompEvents()) {
result.append(indent.asSpaces())
result.append("on")
result.append(name)
result.append(" -> ")
result.append(event)
result.append("\n")
}
for (index in 0 until childNodes.length) {
childNodes[index]?.let {
if (it is Element) {
@@ -66,61 +57,39 @@ fun Element.printTree(indent: Int = 0): String {
return result.toString()
}
internal fun Element.setKompAttribute(attributeName: String, value: String?) {
//val attributeName = name.lowercase()
internal fun Element.setKompAttribute(name: String, value: String?) {
// val setAttrs: MutableSet<String> = getKompAttributes()
// setAttrs.add(name)
//getNewAttributes().add(name)
if (value == null || value.isBlank()) {
if (this is HTMLInputElement) {
when (attributeName) {
"checked" -> {
checked = false
}
/*
"class" -> {
className = ""
}
*/
"value" -> {
this.value = ""
}
else -> {
removeAttribute(attributeName)
}
}
checked = false
} else {
removeAttribute(attributeName)
removeAttribute(name)
}
} else {
if (this is HTMLInputElement) {
when (attributeName) {
when (name) {
"checked" -> {
checked = "checked" == value
}
/*
"class" -> {
className = value
}
*/
"value" -> {
this.value = value
}
else -> {
setAttribute(attributeName, value)
setAttribute(name, value)
}
}
} else if (this.getAttribute(attributeName) != value) {
setAttribute(attributeName, value)
} else if (this.getAttribute(name) != value) {
setAttribute(name, value)
}
}
}
internal fun Element.clearKompEvents() {
val events = getKompEvents()
for ((name, event) in getKompEvents()) {
removeEventListener(name, event)
}
events.clear()
}
internal fun Element.setKompEvent(name: String, event: (Event) -> Unit) {
val eventName: String = if (name.startsWith("on")) {
name.substring(2)
@@ -128,22 +97,9 @@ internal fun Element.setKompEvent(name: String, event: (Event) -> Unit) {
name
}
getKompEvents()[eventName] = event
this.addEventListener(eventName, event)
}
internal fun Element.getKompEvents(): MutableMap<String, (Event) -> Unit> {
var result: MutableMap<String, (Event) -> Unit>? = this.asDynamic()["komp-events"] as MutableMap<String, (Event) -> Unit>?
if (result == null) {
result = mutableMapOf()
this.asDynamic()["komp-events"] = result
}
return result
}
internal fun Element.findElementIndex(): Int {
val childNodes = parentElement?.children
if (childNodes != null) {

View File

@@ -1,64 +0,0 @@
package nl.astraeus.komp
import org.w3c.dom.Node
import org.w3c.dom.get
data class ElementIndex(
val parent: Node,
var childIndex: Int,
var setAttr: MutableSet<String> = mutableSetOf()
) {
override fun toString(): String {
return "${parent.nodeName}[$childIndex]"
}
}
fun ArrayList<ElementIndex>.currentParent(): Node {
this.lastOrNull()?.let {
return it.parent
}
throw IllegalStateException("currentParent should never be null!")
}
fun ArrayList<ElementIndex>.currentElement(): Node? {
this.lastOrNull()?.let {
return it.parent.childNodes[it.childIndex]
}
return null
}
fun ArrayList<ElementIndex>.currentPosition(): ElementIndex? {
return if (this.size < 2) {
null
} else {
this[this.size - 2]
}
}
fun ArrayList<ElementIndex>.nextElement() {
this.lastOrNull()?.let {
it.setAttr.clear()
it.childIndex++
}
}
fun ArrayList<ElementIndex>.pop() {
this.removeLast()
}
fun ArrayList<ElementIndex>.push(element: Node) {
this.add(ElementIndex(element, 0))
}
fun ArrayList<ElementIndex>.replace(new: Node) {
if (this.currentElement() != null) {
this.currentElement()?.parentElement?.replaceChild(
new,
this.currentElement()!!
)
} else {
this.last().parent.appendChild(new)
}
}

View File

@@ -9,10 +9,10 @@ import kotlinx.html.TagConsumer
import kotlinx.html.Unsafe
import org.w3c.dom.Element
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.HTMLSpanElement
import org.w3c.dom.Node
import org.w3c.dom.asList
import org.w3c.dom.events.Event
import org.w3c.dom.get
private var currentElement: Element? = null
@@ -26,18 +26,60 @@ interface HtmlConsumer : TagConsumer<Element> {
fun FlowOrMetaDataOrPhrasingContent.currentElement(): Element =
currentElement ?: error("No current element defined!")
private data class ElementIndex(
val parent: Node,
var childIndex: Int
)
private fun ArrayList<ElementIndex>.currentParent(): Node {
this.lastOrNull()?.let {
return it.parent
}
throw IllegalStateException("currentParent should never be null!")
}
private fun ArrayList<ElementIndex>.currentElement(): Node? {
this.lastOrNull()?.let {
return it.parent.childNodes[it.childIndex]
}
return null
}
private fun ArrayList<ElementIndex>.nextElement() {
this.lastOrNull()?.let {
it.childIndex++
}
}
private fun ArrayList<ElementIndex>.pop() {
this.removeLast()
}
private fun ArrayList<ElementIndex>.push(element: Node) {
this.add(ElementIndex(element, 0))
}
private fun ArrayList<ElementIndex>.replace(new: Node) {
if (this.currentElement() != null) {
this.currentElement()?.parentElement?.replaceChild(new, this.currentElement()!!)
} else {
this.last().parent.appendChild(new)
}
}
private fun Node.asElement() = this as? HTMLElement
class HtmlBuilder(
private val komponent: Komponent?,
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
private var currentNode: Node? = null
private var firstTag: Boolean = true
var currentNode: Node? = null
var root: Element? = null
init {
@@ -51,18 +93,9 @@ class HtmlBuilder(
) {
currentPosition.replace(komponent.element!!)
if (Komponent.logRenderEvent) {
console.log(
"Skipped include $komponent, memoize hasn't changed"
)
console.log("Skipped include $komponent, memoize hasn't changed")
}
} else {
// current element should become parent
/*
val ce = komponent.element
if (ce != null) {
append(ce as Element)
}
*/
komponent.create(
currentPosition.last().parent as Element,
currentPosition.last().childIndex
@@ -89,42 +122,29 @@ class HtmlBuilder(
}
}
private fun logReplace(msg: () -> String) {
private fun logReplace(msg: String) {
if (Komponent.logReplaceEvent && inDebug) {
console.log(msg.invoke())
console.log(msg)
}
}
override fun onTagStart(tag: Tag) {
logReplace {
"onTagStart, [${tag.tagName}, ${tag.namespace ?: ""}], currentPosition: $currentPosition"
}
logReplace("onTagStart, [${tag.tagName}, ${tag.namespace}], currentPosition: $currentPosition")
currentNode = currentPosition.currentElement()
if (currentNode == null) {
logReplace { "onTagStart, currentNode1: $currentNode" }
logReplace("onTagStart, currentNode1: $currentNode")
currentNode = if (tag.namespace != null) {
document.createElementNS(tag.namespace, tag.tagName)
} else {
document.createElement(tag.tagName)
}
logReplace { "onTagStart, currentElement1.1: $currentNode" }
//logReplace"onTagStart, currentElement1.1: $currentNode")
currentPosition.currentParent().appendChild(currentNode!!)
} else if (
!currentNode?.asElement()?.tagName.equals(tag.tagName, true) ||
(
tag.namespace != null &&
!currentNode?.asElement()?.namespaceURI.equals(tag.namespace, true)
)
) {
logReplace {
"onTagStart, currentElement, namespace: ${currentNode?.asElement()?.namespaceURI} -> ${tag.namespace}"
}
logReplace {
"onTagStart, currentElement, replace: ${currentNode?.asElement()?.tagName} -> ${tag.tagName}"
}
} else {
logReplace("onTagStart, currentElement, namespace: ${currentNode?.asElement()?.namespaceURI} -> ${tag.namespace}")
logReplace("onTagStart, currentElement, replace: ${currentNode?.asElement()?.tagName} -> ${tag.tagName}")
currentNode = if (tag.namespace != null) {
document.createElementNS(tag.namespace, tag.tagName)
@@ -138,76 +158,60 @@ class HtmlBuilder(
currentElement = currentNode as? Element ?: currentElement
if (currentNode is Element) {
if (firstTag) {
logReplace { "Setting root: $currentNode" }
if (root == null) {
//logReplace"Setting root: $currentNode")
root = currentNode as Element
firstTag = false
}
currentElement?.clearKompEvents()
// if currentElement = checkbox make sure it's cleared
(currentElement as? HTMLInputElement)?.checked = false
currentPosition.lastOrNull()?.setAttr?.clear()
for (entry in tag.attributesEntries) {
currentElement!!.setKompAttribute(entry.key, entry.value)
currentPosition.lastOrNull()?.setAttr?.add(entry.key)
val attributeName = entry.key.lowercase()
currentElement!!.setKompAttribute(attributeName, entry.value)
}
if (tag.namespace != null) {
//logReplace"onTagStart, same node type")
(currentNode as? Element)?.innerHTML = ""
}
}
currentPosition.push(currentNode!!)
}
private fun checkTag(source: String, tag: Tag) {
private fun checkTag(tag: Tag) {
check(currentElement != null) {
js("debugger;")
"No current tag ($source)"
js("debugger")
"No current tag"
}
check(currentElement?.tagName.equals(tag.tagName, ignoreCase = true)) {
js("debugger;")
"Wrong current tag ($source), got: ${tag.tagName} expected ${currentElement?.tagName}"
js("debugger")
"Wrong current tag"
}
}
override fun onTagAttributeChange(
tag: Tag,
attribute: String,
value: String?
) {
logReplace { "onTagAttributeChange, ${tag.tagName} [$attribute, $value]" }
override fun onTagAttributeChange(tag: Tag, attribute: String, value: String?) {
logReplace("onTagAttributeChange, ${tag.tagName} [$attribute, $value]")
if (Komponent.enableAssertions) {
checkTag("onTagAttributeChange", tag)
checkTag(tag)
}
currentElement?.setKompAttribute(attribute, value)
if (value == null || value.isEmpty()) {
currentPosition.currentPosition()?.setAttr?.remove(attribute)
} else {
currentPosition.currentPosition()?.setAttr?.add(attribute)
}
val attributeName = attribute.lowercase()
currentElement?.setKompAttribute(attributeName, value)
}
override fun onTagEvent(
tag: Tag,
event: String,
value: (kotlinx.html.org.w3c.dom.events.Event) -> Unit
) {
logReplace { "onTagEvent, ${tag.tagName} [$event, $value]" }
override fun onTagEvent(tag: Tag, event: String, value: (Event) -> Unit) {
logReplace("onTagEvent, ${tag.tagName} [$event, $value]")
if (Komponent.enableAssertions) {
checkTag("onTagEvent", tag)
checkTag(tag)
}
currentElement?.setKompEvent(event.lowercase(), value.asDynamic())
currentElement?.setKompEvent(event.lowercase(), value)
}
override fun onTagEnd(tag: Tag) {
logReplace {
"onTagEnd, [${tag.tagName}, ${tag.namespace}], currentPosition: $currentPosition"
}
if (exceptionThrown) {
return
}
@@ -219,36 +223,11 @@ class HtmlBuilder(
}
if (Komponent.enableAssertions) {
checkTag("onTagEnd", tag)
}
if (currentElement != null) {
val setAttrs: Set<String> = currentPosition.currentPosition()?.setAttr ?: setOf()
// remove attributes that where not set
val element = currentElement
if (element?.hasAttributes() == true) {
for (index in 0 until element.attributes.length) {
val attribute = element.attributes[index]
if (attribute?.name != null) {
val attr = attribute.name
if (
!setAttrs.contains(attr) &&
attr != "style"
) {
element.setKompAttribute(attr, null)
}
}
}
}
checkTag(tag)
}
currentPosition.pop()
currentNode = currentPosition.currentElement()
currentElement = currentNode as? Element ?: currentElement
currentPosition.nextElement()
currentElement = currentElement?.parentElement as? HTMLElement
@@ -322,10 +301,7 @@ class HtmlBuilder(
//logReplace"onTagContentUnsafe, namespace: [$namespace]")
if (Komponent.unsafeMode == UnsafeMode.UNSAFE_ALLOWED ||
(
Komponent.unsafeMode == UnsafeMode.UNSAFE_SVG_ONLY &&
namespace == "http://www.w3.org/2000/svg"
)
(Komponent.unsafeMode == UnsafeMode.UNSAFE_SVG_ONLY && namespace == "http://www.w3.org/2000/svg")
) {
if (currentElement?.innerHTML != textContent) {
currentElement?.innerHTML += textContent
@@ -351,13 +327,13 @@ class HtmlBuilder(
currentPosition.nextElement()
}
fun onTagError(tag: Tag, exception: Throwable) {
override fun onTagError(tag: Tag, exception: Throwable) {
exceptionThrown = true
if (exception !is KomponentException) {
val position = mutableListOf<Element>()
var ce = currentElement
while (ce != null) {
while(ce != null) {
position.add(ce)
ce = ce.parentElement
}
@@ -375,7 +351,6 @@ class HtmlBuilder(
}
builder.append(" ")
}
throw KomponentException(
komponent,
currentElement,
@@ -391,15 +366,13 @@ class HtmlBuilder(
override fun finalize(): Element {
//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")
}
companion object {
fun create(content: HtmlBuilder.() -> Unit): Element {
val container = document.createElement("div") as HTMLElement
val consumer = HtmlBuilder(null, container)
val consumer = HtmlBuilder(null, container, 0)
content.invoke(consumer)
return consumer.root ?: error("No root element found after render!")
}

View File

@@ -7,7 +7,6 @@ import org.w3c.dom.HTMLElement
import org.w3c.dom.get
private var currentKomponent: Komponent? = null
fun FlowOrMetaDataOrPhrasingContent.currentKomponent(): Komponent =
currentKomponent ?: error("No current komponent defined! Only call from render code!")
@@ -111,25 +110,8 @@ abstract class Komponent {
*
* HTMLBuilder.render() is called 1st time the component is rendered, after that this
* method will be called
*
* @deprecated
*/
@Deprecated(
"Deprecated to avoid confusing with requestUpdate, use renderUpdate instead",
ReplaceWith("renderUpdate"),
level = DeprecationLevel.WARNING
)
protected fun update() {
refresh()
}
/**
* This function can be overwritten if you know how to update the Komponent yourself
*
* HTMLBuilder.render() is called 1st time the component is rendered, after that this
* method will be called
*/
open fun renderUpdate() {
open fun update() {
refresh()
}
@@ -155,6 +137,7 @@ abstract class Komponent {
}
}
val builder = HtmlBuilder(this, parent, childIndex)
builder.root = null
try {
currentKomponent = this
@@ -245,7 +228,7 @@ abstract class Komponent {
if (next.memoizeChanged()) {
next.onBeforeUpdate()
next.renderUpdate()
next.update()
next.updateMemoizeHash()
next.onAfterUpdate()
} else if (logRenderEvent) {

View File

@@ -31,7 +31,7 @@ class MutableCollectionStateDelegate<T>(
}
// todo: return iterator wrapper to update at changes?
//override fun iterator(): MutableIterator<T> = collection.iterator()
// override fun iterator(): MutableIterator<T> = collection.iterator()
override fun remove(element: T): Boolean {
komponent.requestUpdate()

View File

@@ -14,9 +14,9 @@ interface Delegate<T> {
property: KProperty<*>,
value: T
)
}
open class StateDelegate<T>(
val komponent: Komponent,
initialValue: T
@@ -51,6 +51,6 @@ open class StateDelegate<T>(
inline fun <reified T> Komponent.state(
initialValue: T
): Delegate<T> = StateDelegate(
this,
initialValue
)
this,
initialValue
)

View File

@@ -1,9 +1,7 @@
package nl.astraeus.komp
import kotlinx.browser.document
import kotlinx.html.DIV
import kotlinx.html.InputType
import kotlinx.html.classes
import kotlinx.html.div
import kotlinx.html.i
import kotlinx.html.id
@@ -39,7 +37,6 @@ class Child1 : Komponent() {
class Child2 : Komponent() {
override fun HtmlBuilder.render() {
div {
id ="1234"
+"Child 2"
}
}
@@ -126,7 +123,7 @@ class IncludeKomponent(
}
class ReplaceKomponent : Komponent() {
val includeKomponent = IncludeKomponent("Other text")
val includeKomponent = IncludeKomponent()
var includeSpan = true
override fun generateMemoizeHash(): Int = includeSpan.hashCode() * 7 + includeKomponent.generateMemoizeHash()
@@ -137,8 +134,20 @@ class ReplaceKomponent : Komponent() {
div {
if (includeSpan) {
for (index in 0 ..< 3) {
extracted(index)
span {
i("fas fa-eye") {
+"span1"
}
}
span {
i("fas fa-eye") {
+"span2"
}
}
span {
i("fas fa-eye") {
+"span3"
}
}
}
@@ -146,14 +155,6 @@ class ReplaceKomponent : Komponent() {
}
}
}
private fun HtmlBuilder.extracted(index: Int) {
span {
i("fas fa-eye") {
+ ("span" + (index+1))
}
}
}
}
class TestUpdate {
@@ -208,8 +209,7 @@ class TestUpdate {
fun testCreate() {
var elemTest: Element? = null
val element = HtmlBuilder.create {
div(classes = "div_class") {
classes = classes + "bla'"
div("div_class") {
id = "123"
+"Test"