Compare commits

...

17 Commits

Author SHA1 Message Date
88cf5b8533 Bump version to 1.2.10, simplify attribute and event handling in ElementExtensions, and remove deprecated logic in HtmlBuilder.
Some checks failed
Gradle CI / build (push) Has been cancelled
2025-10-18 16:24:52 +02:00
d3fef98203 v.1.2.9 - Fix memory leak
Some checks failed
Gradle CI / build (push) Has been cancelled
2025-09-19 19:19:07 +02:00
191b23ed51 Bump version to 1.2.8, add TestStyleUpdate for verifying style attribute updates and removals, and simplify style attribute handling in HtmlBuilder.
Some checks failed
Gradle CI / build (push) Has been cancelled
2025-06-12 19:22:31 +02:00
66b4633e6b Update README to reflect version 1.2.7 release on Maven Central
Some checks failed
Gradle CI / build (push) Has been cancelled
2025-05-30 12:26:37 +02:00
177d96975a Bump version to 1.2.7, add TestSvg for testing SVG rendering, and remove redundant class attribute handling in ElementExtensions.
Some checks failed
Gradle CI / build (push) Has been cancelled
2025-05-30 12:22:21 +02:00
e97b6966ba Release v. 1.2.6
Some checks failed
Gradle CI / build (push) Has been cancelled
2025-05-03 10:13:18 +02:00
283c19defb Fix and test class attribute handling in ElementExtensions.
Some checks failed
Gradle CI / build (push) Has been cancelled
Re-enabled previously commented-out logic for managing the "class" attribute in ElementExtensions to ensure proper updates and removals. Added comprehensive unit tests in `TestClassUpdate` to verify behavior, including class addition, removal, and name changes. Bumped the project version to 1.2.6.
2025-04-20 17:50:15 +02:00
0d2f2146e9 Add guidelines and tests for Komponent development
Some checks failed
Gradle CI / build (push) Has been cancelled
Introduce detailed development guidelines for the Kotlin Komponent (Komp) library, covering build instructions, testing, optimization, and code style. Additionally, add a new test case for verifying dynamic row insertion in a table component.
2025-04-18 14:52:48 +02:00
497ca14c27 Github action
Some checks failed
Gradle CI / build (push) Has been cancelled
2025-04-06 15:35:26 +02:00
a5e7963412 Migrate to com.vanniktech.maven.publish publish plugin, update gradle, publish to maven central.
Some checks are pending
Gradle CI / build (push) Waiting to run
2025-04-06 15:22:12 +02:00
6f37c879c2 Migrate to com.vanniktech.maven.publish publish plugin, update gradle, publish to maven central.
Some checks are pending
Gradle CI / build (push) Waiting to run
2025-04-06 15:13:49 +02:00
ab5689133f Migrate to com.vanniktech.maven.publish publish plugin, update gradle, publish to maven central. 2025-04-06 15:13:38 +02:00
9e08601bb7 Update readme.md 2025-04-06 15:13:16 +02:00
c68a024552 Upgrade Kotlin version and simplify Maven signing tasks.
Some checks failed
Gradle CI / build (push) Has been cancelled
Updated Kotlin multiplatform plugin to version 2.1.10. Removed redundant Maven publication task dependencies and streamlined the signing process for better maintainability.
2025-03-17 18:20:16 +00:00
46bb07d567 Version 1.2.5-SNAPSHOT
Some checks failed
Gradle CI / build (push) Has been cancelled
2024-10-23 15:36:53 +02:00
a1021e5cda Version 1.2.4
Some checks are pending
Gradle CI / build (push) Waiting to run
Add WASM support and optimize event handling
2024-10-23 15:35:05 +02:00
a5b938aa27 1.2.4-SNAPSHOT 2024-07-15 21:11:27 +02:00
15 changed files with 608 additions and 277 deletions

View File

@@ -9,9 +9,9 @@ jobs:
steps:
- uses: actions/checkout@v1
- name: Set up JDK 1.8
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 1.8
java-version: 11
- name: Build with Gradle
run: ./gradlew build

144
.junie/guidelines.md Normal file
View File

@@ -0,0 +1,144 @@
# Kotlin Komponent (Komp) Development Guidelines
This document provides specific information for developing with the Kotlin Komponent (Komp) library, a component-based UI library for Kotlin/JS.
## Build/Configuration Instructions
### Prerequisites
- Kotlin 2.1.10 or higher
- Gradle 7.0 or higher
### Building the Project
The project uses Kotlin Multiplatform with a focus on JavaScript (and potentially WebAssembly in the future).
```bash
# Build the project
./gradlew build
# Build only JS target
./gradlew jsJar
# Publish to Maven Local for local development
./gradlew publishToMavenLocal
```
### Configuration
The project uses the following Gradle plugins:
- Kotlin Multiplatform
- Maven Publish
- Dokka for documentation
Key configuration files:
- `build.gradle.kts` - Main build configuration
- `gradle.properties` - Contains publishing credentials and signing configuration
- `settings.gradle.kts` - Project settings and repository configuration
## Testing Information
### Running Tests
Tests are written using the Kotlin Test library and run with Karma using Chrome Headless.
```bash
# Run all tests
./gradlew jsTest
# Run browser tests
./gradlew jsBrowserTest
```
### Test Structure
Tests are located in the `src/jsTest` directory. The project uses the standard Kotlin Test library with annotations:
```kotlin
@Test
fun testSomething() {
// Test code here
}
```
### Writing New Tests
When writing tests for Komponents:
1. Create a test class in the `src/jsTest/kotlin/nl/astraeus/komp` directory
2. Use the `@Test` annotation for test methods
3. For UI component tests:
- Create a test DOM element using `document.createElement("div")`
- Create your Komponent instance
- Render it using `Komponent.create(element, komponent)`
- Modify state and call `requestImmediateUpdate()` to test updates
- Verify the DOM structure using assertions
### Example Test
Here's a simple test example:
```kotlin
@Test
fun testSimpleComponent() {
// Create a test component
val component = SimpleKomponent()
val div = document.createElement("div") as HTMLDivElement
// Render it
Komponent.create(div, component)
// Verify initial state
assertEquals("Hello", div.querySelector("div")?.textContent)
// Update state and re-render
component.hello = false
component.requestImmediateUpdate()
// Verify updated state
assertEquals("Good bye", div.querySelector("span")?.textContent)
}
```
## Additional Development Information
### Project Structure
- `src/commonMain` - Common Kotlin code
- `src/jsMain` - JavaScript-specific implementation
- `src/wasmJsMain` - WebAssembly JavaScript implementation (experimental)
- `src/jsTest` - JavaScript tests
### Key Components
1. **Komponent** - Base class for all UI components
- Override `HtmlBuilder.render()` to define the component's UI
- Use `requestUpdate()` for scheduled updates
- Use `requestImmediateUpdate()` for immediate updates
- Override `generateMemoizeHash()` to optimize re-renders
2. **HtmlBuilder** - Handles DOM creation and updates
- Uses a virtual DOM-like approach to update only what changed
- Supports including child components with `include()`
- Handles attribute and event binding
3. **ElementExtensions** - Utility functions for DOM manipulation
### Optimization Features
- **Memoization**: Components can implement `generateMemoizeHash()` to avoid unnecessary re-renders
- **Batched Updates**: Multiple `requestUpdate()` calls are batched for performance
- **Efficient DOM Updates**: Only changed elements are updated in the DOM
### Error Handling
The library provides detailed error information when rendering fails:
- Set custom error handlers with `Komponent.setErrorHandler()`
- Enable debug logging with `Komponent.logRenderEvent = true`
- Use `debug {}` blocks in render functions for additional validation
### Code Style
- Follow Kotlin's official code style (`kotlin.code.style=official`)
- Use functional programming patterns where appropriate
- Prefer immutable state when possible
- Use descriptive names for components and methods

View File

@@ -1,190 +1,139 @@
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
@file:OptIn(ExperimentalWasmDsl::class)
import com.vanniktech.maven.publish.SonatypeHost
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
plugins {
kotlin("multiplatform") version "2.0.0"
id("maven-publish")
id("signing")
id("org.jetbrains.dokka") version "1.5.31"
kotlin("multiplatform") version "2.1.10"
signing
id("org.jetbrains.dokka") version "2.0.0"
id("com.vanniktech.maven.publish") version "0.31.0"
}
group = "nl.astraeus"
version = "1.2.3-SNAPSHOT"
version = "1.2.10"
repositories {
mavenCentral()
maven {
name = "Sonatype Releases"
url = uri("https://central.sonatype.com/api/v1/publisher/deployments/download/")
}
}
/*
tasks.withType<Test>(Test::class.java) {
useJUnitPlatform()
}
*/
kotlin {
js {
browser {
testTask {
useKarma {
useChromiumHeadless()
useChromeHeadless()
}
}
}
}
/* @OptIn(ExperimentalWasmDsl::class)
wasmJs {
//moduleName = project.name
browser()
/* wasmJs {
//moduleName = project.name
browser()
mavenPublication {
groupId = group as String
pom { name = "${project.name}-wasm-js" }
}
}*/
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()
/*
@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")
api("org.jetbrains.kotlinx:kotlinx-html:0.12.0")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
val jsMain by getting
val jsTest by getting {
dependencies {
implementation(kotlin("test-js"))
implementation(kotlin("test"))
}
}
//val wasmJsMain by getting
}
}
extra["PUBLISH_GROUP_ID"] = group
extra["PUBLISH_VERSION"] = version
extra["PUBLISH_ARTIFACT_ID"] = name
// Stub secrets to let the project sync and build without the publication values set up
val signingKeyId: String? by project
val signingPassword: String? by project
val signingSecretKeyRingFile: String? by project
val ossrhUsername: String? by project
val ossrhPassword: String? by project
extra["signing.keyId"] = signingKeyId
extra["signing.password"] = signingPassword
extra["signing.secretKeyRingFile"] = signingSecretKeyRingFile
extra["ossrhUsername"] = ossrhUsername
extra["ossrhPassword"] = ossrhPassword
val javadocJar by tasks.registering(Jar::class) {
archiveClassifier.set("javadoc")
}
publishing {
repositories {
mavenLocal()
maven {
name = "releases"
// change to point to your repo, e.g. http://my.org/repo
setUrl("https://reposilite.astraeus.nl/releases")
credentials {
val reposiliteUsername: String? by project
val reposilitePassword: String? by project
username = reposiliteUsername
password = reposilitePassword
}
}
maven {
name = "snapshots"
// change to point to your repo, e.g. http://my.org/repo
setUrl("https://reposilite.astraeus.nl/snapshots")
credentials {
val reposiliteUsername: String? by project
val reposilitePassword: String? by project
username = reposiliteUsername
password = reposilitePassword
}
}
maven {
name = "sonatype"
setUrl("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2")
credentials {
username = ossrhUsername
password = ossrhPassword
}
}
maven {
name = "gitea"
setUrl("https://gitea.astraeus.nl/api/packages/rnentjes/maven")
credentials() {
val giteaUsername: kotlin.String? by project
val giteaPassword: kotlin.String? by project
credentials {
val giteaUsername: String? by project
val giteaPassword: String? by project
username = giteaUsername
password = giteaPassword
}
}
}
}
// Configure all publications
publications.withType<MavenPublication> {
// Stub javadoc.jar artifact
artifact(javadocJar.get())
// Provide artifacts information requited by Maven Central
pom {
name.set("kotlin-komponent")
description.set("Kotlin komponent")
url.set("https://github.com/rnentjes/komponent")
licenses {
license {
name.set("MIT")
url.set("https://opensource.org/licenses/MIT")
}
}
developers {
developer {
id.set("rnentjes")
name.set("Rien Nentjes")
email.set("info@nentjes.com")
}
}
scm {
url.set("https://github.com/rnentjes/komponent")
}
}
}
tasks.withType<AbstractPublishToMaven> {
dependsOn(tasks.withType<Sign>())
}
signing {
sign(publishing.publications)
}
tasks.named<Task>("signJsPublication") {
dependsOn(tasks.named<Task>("publishKotlinMultiplatformPublicationToMavenLocal"))
}
mavenPublishing {
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
tasks.named<Task>("publishJsPublicationToReleasesRepository") {
dependsOn(tasks.named<Task>("signKotlinMultiplatformPublication"))
}
signAllPublications()
tasks.named<Task>("publishKotlinMultiplatformPublicationToMavenLocalRepository") {
dependsOn(tasks.named<Task>("signJsPublication"))
}
coordinates(group.toString(), name, version.toString())
tasks.named<Task>("publishKotlinMultiplatformPublicationToReleasesRepository") {
dependsOn(tasks.named<Task>("signJsPublication"))
}
tasks.named<Task>("publishKotlinMultiplatformPublicationToSonatypeRepository") {
dependsOn(tasks.named<Task>("signJsPublication"))
pom {
name = "kotlin-komponent"
description = "Kotlin komponent"
inceptionYear = "2017"
url = "https://github.com/rnentjes/komponent"
licenses {
license {
name = "MIT"
url = "https://opensource.org/licenses/MIT"
}
}
developers {
developer {
id = "rnentjes"
name = "Rien Nentjes"
email = "info@nentjes.com"
}
}
scm {
url = "https://github.com/rnentjes/komponent"
}
}
}

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-8.5-all.zip
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStorePath=wrapper/dists

View File

@@ -8,6 +8,6 @@ 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"
Available on maven central: "nl.astraeus:kotlin-komponent-js:1.2.7"
Some getting started documentation can be found [here](docs/getting-started.md)
Some getting started documentation can be found [here](docs/getting-started.md)

View File

@@ -1,2 +1,17 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
rootProject.name = "kotlin-komponent"

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,59 +57,52 @@ fun Element.printTree(indent: Int = 0): String {
return result.toString()
}
internal fun Element.setKompAttribute(attributeName: String, value: String?) {
//val attributeName = name.lowercase()
if (value == null || value.isBlank()) {
if (this is HTMLInputElement) {
when (attributeName) {
"checked" -> {
checked = false
}
/*
"class" -> {
className = ""
}
*/
"value" -> {
this.value = ""
}
else -> {
removeAttribute(attributeName)
}
internal fun Element.setKompAttribute(attributeName: String, value: String) {
if (this is HTMLInputElement) {
when (attributeName) {
"checked" -> {
checked = "checked" == value
}
} else {
removeAttribute(attributeName)
}
} else {
if (this is HTMLInputElement) {
when (attributeName) {
"checked" -> {
checked = "checked" == value
}
/*
"class" -> {
className = value
}
*/
"value" -> {
this.value = value
}
else -> {
setAttribute(attributeName, value)
}
"class" -> {
className = value
}
"value" -> {
this.value = value
}
else -> {
setAttribute(attributeName, value)
}
} else if (this.getAttribute(attributeName) != value) {
setAttribute(attributeName, value)
}
} else if (this.getAttribute(attributeName) != value) {
setAttribute(attributeName, value)
}
}
internal fun Element.clearKompEvents() {
val events = getKompEvents()
for ((name, event) in getKompEvents()) {
removeEventListener(name, event)
internal fun Element.clearKompAttribute(attributeName: String) {
if (this is HTMLInputElement) {
when (attributeName) {
"checked" -> {
checked = false
}
"class" -> {
className = ""
}
"value" -> {
this.value = ""
}
else -> {
removeAttribute(attributeName)
}
}
} else {
removeAttribute(attributeName)
}
events.clear()
}
internal fun Element.setKompEvent(name: String, event: (Event) -> Unit) {
@@ -128,22 +112,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

@@ -6,7 +6,6 @@ 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]"
@@ -39,7 +38,6 @@ fun ArrayList<ElementIndex>.currentPosition(): ElementIndex? {
fun ArrayList<ElementIndex>.nextElement() {
this.lastOrNull()?.let {
it.setAttr.clear()
it.childIndex++
}
}

View File

@@ -26,7 +26,7 @@ interface HtmlConsumer : TagConsumer<Element> {
fun FlowOrMetaDataOrPhrasingContent.currentElement(): Element =
currentElement ?: error("No current element defined!")
private fun Node.asElement() = this as? HTMLElement
private fun Node?.asElement() = this as? HTMLElement
class HtmlBuilder(
private val komponent: Komponent?,
@@ -57,12 +57,6 @@ class HtmlBuilder(
}
} 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
@@ -100,39 +94,26 @@ class HtmlBuilder(
"onTagStart, [${tag.tagName}, ${tag.namespace ?: ""}], currentPosition: $currentPosition"
}
currentNode = currentPosition.currentElement()
currentNode = if (tag.namespace != null) {
document.createElementNS(tag.namespace, tag.tagName)
} else {
document.createElement(tag.tagName)
}
if (currentNode == null) {
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" }
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)
} else {
document.createElement(tag.tagName)
}
currentPosition.replace(currentNode!!)
currentPosition.replace(currentNode!!)
}
currentElement = currentNode as? Element ?: currentElement
@@ -144,15 +125,8 @@ class HtmlBuilder(
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)
currentElement?.setKompAttribute(entry.key, entry.value)
}
}
@@ -181,11 +155,10 @@ class HtmlBuilder(
checkTag("onTagAttributeChange", tag)
}
currentElement?.setKompAttribute(attribute, value)
if (value == null || value.isEmpty()) {
currentPosition.currentPosition()?.setAttr?.remove(attribute)
currentElement?.clearKompAttribute(attribute)
} else {
currentPosition.currentPosition()?.setAttr?.add(attribute)
currentElement?.setKompAttribute(attribute, value)
}
}
@@ -200,7 +173,7 @@ class HtmlBuilder(
checkTag("onTagEvent", tag)
}
currentElement?.setKompEvent(event.lowercase(), value.asDynamic())
currentElement?.setKompEvent(event.lowercase(), value)
}
override fun onTagEnd(tag: Tag) {
@@ -222,28 +195,6 @@ class HtmlBuilder(
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)
}
}
}
}
}
currentPosition.pop()
currentNode = currentPosition.currentElement()
@@ -327,9 +278,7 @@ class HtmlBuilder(
namespace == "http://www.w3.org/2000/svg"
)
) {
if (currentElement?.innerHTML != textContent) {
currentElement?.innerHTML += textContent
}
currentElement?.innerHTML += textContent.trim()
} else if (currentElement?.textContent != textContent) {
currentElement?.textContent = textContent
}

View File

@@ -191,6 +191,7 @@ abstract class Komponent {
private var scheduledForUpdate = mutableSetOf<Komponent>()
private var interceptor: (Komponent, () -> Unit) -> Unit = { _, block -> block() }
var logUpdateEvent = false
var logRenderEvent = false
var logReplaceEvent = false
var enableAssertions = false
@@ -214,7 +215,7 @@ abstract class Komponent {
scheduledForUpdate.add(komponent)
if (updateCallback == null) {
window.setTimeout({
updateCallback = window.setTimeout({
runUpdate()
}, 0)
}
@@ -244,6 +245,9 @@ abstract class Komponent {
val memoizeHash = next.generateMemoizeHash()
if (next.memoizeChanged()) {
if (logUpdateEvent) {
console.log("Rendering", next)
}
next.onBeforeUpdate()
next.renderUpdate()
next.updateMemoizeHash()

View File

@@ -0,0 +1,73 @@
package nl.astraeus.komp
import kotlinx.browser.document
import kotlinx.html.div
import kotlinx.html.classes
import org.w3c.dom.HTMLDivElement
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
/**
* Test class for verifying class attribute updates and removals
*/
class ClassKomponent : Komponent() {
var includeClass = true
var className = "test-class"
override fun HtmlBuilder.render() {
div {
if (includeClass) {
classes = setOf(className)
}
+"Content"
}
}
}
class TestClassUpdate {
@Test
fun testClassRemoval() {
// Create a test component
val classComponent = ClassKomponent()
val div = document.createElement("div") as HTMLDivElement
// Render it
Komponent.create(div, classComponent)
// Verify initial state - should have the class
var contentDiv = div.querySelector("div")
println("[DEBUG_LOG] Initial DOM: ${div.printTree()}")
assertTrue(contentDiv?.classList?.contains("test-class") ?: false, "Div should have the class initially")
// Update to remove the class
classComponent.includeClass = false
classComponent.requestImmediateUpdate()
// Verify the class was removed
contentDiv = div.querySelector("div")
println("[DEBUG_LOG] After class removal: ${div.printTree()}")
assertFalse(contentDiv?.classList?.contains("test-class") ?: true, "Class should be removed after update")
// Add the class back
classComponent.includeClass = true
classComponent.requestImmediateUpdate()
// Verify the class was added back
contentDiv = div.querySelector("div")
println("[DEBUG_LOG] After class added back: ${div.printTree()}")
assertTrue(contentDiv?.classList?.contains("test-class") ?: false, "Class should be added back")
// Change the class name
classComponent.className = "new-class"
classComponent.requestImmediateUpdate()
// Verify the class was changed
contentDiv = div.querySelector("div")
println("[DEBUG_LOG] After class name change: ${div.printTree()}")
assertFalse(contentDiv?.classList?.contains("test-class") ?: true, "Old class should be removed")
assertTrue(contentDiv?.classList?.contains("new-class") ?: false, "New class should be added")
}
}

View File

@@ -0,0 +1,100 @@
package nl.astraeus.komp
import kotlinx.browser.document
import kotlinx.html.div
import kotlinx.html.table
import kotlinx.html.tbody
import kotlinx.html.td
import kotlinx.html.tr
import org.w3c.dom.HTMLDivElement
import org.w3c.dom.HTMLTableElement
import org.w3c.dom.HTMLTableRowElement
import org.w3c.dom.HTMLTableCellElement
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
/**
* A Komponent that renders a table with rows
*/
class TableKomponent : Komponent() {
val rows = mutableListOf<RowKomponent>()
fun addRow(text: String) {
rows.add(RowKomponent(text))
requestImmediateUpdate()
}
override fun HtmlBuilder.render() {
div {
table {
tbody {
for (row in rows) {
include(row)
}
}
}
}
}
}
/**
* A Komponent that represents a single row in a table
*/
class RowKomponent(val text: String) : Komponent() {
override fun generateMemoizeHash(): Int = text.hashCode()
override fun HtmlBuilder.render() {
tr {
td {
+text
}
}
}
}
/**
* Test class for inserting rows in the DOM with a Komponent
*/
class TestInsert {
@Test
fun testInsertRow() {
// Create a test component
val tableComponent = TableKomponent()
val div = document.createElement("div") as HTMLDivElement
// Render it
Komponent.create(div, tableComponent)
// Verify initial state - should be an empty table
val table = div.querySelector("table")
assertNotNull(table, "Table should be rendered")
val initialRows = table.querySelectorAll("tr")
assertEquals(0, initialRows.length, "Table should initially have no rows")
// Add a row and verify it was inserted
tableComponent.addRow("First Row")
// Verify the row was added
val rowsAfterFirstInsert = div.querySelector("table")?.querySelectorAll("tr")
assertEquals(1, rowsAfterFirstInsert?.length, "Table should have one row after insertion")
val firstRowCell = div.querySelector("table")?.querySelector("tr td")
assertNotNull(firstRowCell, "First row cell should exist")
assertEquals("First Row", firstRowCell.textContent, "Row content should match")
// Add another row and verify it was inserted
tableComponent.addRow("Second Row")
// Verify both rows are present
val rowsAfterSecondInsert = div.querySelector("table")?.querySelectorAll("tr")
assertEquals(2, rowsAfterSecondInsert?.length, "Table should have two rows after second insertion")
val allCells = div.querySelector("table")?.querySelectorAll("tr td")
assertEquals(2, allCells?.length, "Table should have two cells")
assertEquals("First Row", allCells?.item(0)?.textContent, "First row content should match")
assertEquals("Second Row", allCells?.item(1)?.textContent, "Second row content should match")
// Print the DOM tree for debugging
println("Table DOM: ${div.printTree()}")
}
}

View File

@@ -0,0 +1,79 @@
package nl.astraeus.komp
import kotlinx.browser.document
import kotlinx.html.div
import kotlinx.html.style
import org.w3c.dom.HTMLDivElement
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
/**
* Test class for verifying style attribute updates and removals
*/
class StyleKomponent : Komponent() {
var includeStyle = true
var styleValue = "color: red;"
override fun HtmlBuilder.render() {
div {
if (includeStyle) {
style = styleValue
}
+"Content"
}
}
}
class TestStyleUpdate {
@Test
fun testStyleRemoval() {
// Create a test component
val styleComponent = StyleKomponent()
val div = document.createElement("div") as HTMLDivElement
// Render it
Komponent.create(div, styleComponent)
// Verify initial state - should have the style
var contentDiv = div.querySelector("div")
println("[DEBUG_LOG] Initial DOM: ${div.printTree()}")
assertEquals(
"color: red;",
contentDiv?.getAttribute("style"),
"Div should have the style initially"
)
// Update to remove the style
styleComponent.includeStyle = false
styleComponent.requestImmediateUpdate()
// Verify the style was removed
contentDiv = div.querySelector("div")
println("[DEBUG_LOG] After style removal: ${div.printTree()}")
assertNull(contentDiv?.getAttribute("style"), "Style should be removed after update")
// Add the style back
styleComponent.includeStyle = true
styleComponent.requestImmediateUpdate()
// Verify the style was added back
contentDiv = div.querySelector("div")
println("[DEBUG_LOG] After style added back: ${div.printTree()}")
assertEquals("color: red;", contentDiv?.getAttribute("style"), "Style should be added back")
// Change the style value
styleComponent.styleValue = "color: blue;"
styleComponent.requestImmediateUpdate()
// Verify the style was changed
contentDiv = div.querySelector("div")
println("[DEBUG_LOG] After style value change: ${div.printTree()}")
assertEquals(
"color: blue;",
contentDiv?.getAttribute("style"),
"Style should be updated to new value"
)
}
}

View File

@@ -0,0 +1,50 @@
package nl.astraeus.komp
import kotlinx.browser.document
import kotlinx.html.InputType
import kotlinx.html.classes
import kotlinx.html.div
import kotlinx.html.i
import kotlinx.html.id
import kotlinx.html.input
import kotlinx.html.js.onClickFunction
import kotlinx.html.p
import kotlinx.html.span
import kotlinx.html.svg
import kotlinx.html.table
import kotlinx.html.td
import kotlinx.html.tr
import kotlinx.html.unsafe
import org.w3c.dom.Element
import org.w3c.dom.HTMLDivElement
import kotlin.test.Test
class TestSvgKomponent : Komponent() {
override fun HtmlBuilder.render() {
div {
+"Test"
svg("my-class") {
classes += "added-class"
unsafe {
+"""arc(1,2)"""
}
}
}
}
}
class TestSvg {
@Test
fun testUpdateWithEmpty() {
val div = document.createElement("div") as HTMLDivElement
val rk = TestSvgKomponent()
Komponent.logRenderEvent = true
Komponent.create(div, rk)
println("SvgKomponent: ${div.printTree()}")
}
}

View File

@@ -1,7 +1,6 @@
package nl.astraeus.komp
import kotlinx.browser.document
import kotlinx.html.DIV
import kotlinx.html.InputType
import kotlinx.html.classes
import kotlinx.html.div