xemantic-kotlin-js
0.5.0indexedType-safe DSL for building HTML5 and SVG DOM trees; reactive MVVM utilities with async-friendly state flows; idiomatic extensions for array, map and set collections, plain-object and DOM helpers.
Type-safe DSL for building HTML5 and SVG DOM trees; reactive MVVM utilities with async-friendly state flows; idiomatic extensions for array, map and set collections, plain-object and DOM helpers.
Surfaced from shared tags and platforms — no rankings paid for.
Kotlin Multiplatform library providing type-safe DSL for constructing HTML5 and SVG DOM trees, along with reactive web utilities, targeting JavaScript and WebAssembly runtimes.
A general set of JavaScript utilities for Kotlin/JS and Kotlin/WASM, including idiomatic Kotlin extensions
for JavaScript collections (JsArray, JsMap, JsSet), plain JS objects, DOM utilities,
and a DOM tree builder DSL.
In build.gradle.kts add:
dependencies {
implementation("com.xemantic.kotlin:xemantic-kotlin-js:0.1")
}
globalThis accessThe globalThis property provides access to the JavaScript
globalThis
object as a dynamic value:
val value = globalThis.someGlobalProperty
globalThis.myFlag = true
JsObjectThe JsObject external interface represents a plain JavaScript
object ({}), with operator extensions for bracket access:
val obj = JsObject()
obj["name"] = "Alice"
obj["age"] = 30
val name: String = obj["name"] // "Alice"
obj.isEmpty() // false
obj.isNotEmpty() // true
obj.isNullOrEmpty() // false
Kotlin's experimental JsArray, JsMap, and JsSet types lack many common operations.
This library adds idiomatic Kotlin extensions for all three.
JsArrayval array = jsArrayOf(1, 2, 3)
array.length // 3
array[0] // 1
array[1] = 10 // indexed set
array += 4 // push via += operator
array.push()
array.map { it * }
array.join()
array.isEmpty()
JsMapval map = JsMap<String, Int>()
map["x"] = 1
map["y"] = 2
map["x"] // 1
map.size // 2
map.isEmpty() // false
JsSetval set = jsSetOf("a", "b", "c")
"a" in set // true (contains operator)
"z" in set // false
set.size // 3
set.isEmpty() // false
ItemArrayLike.forEachThe forEach extension on ItemArrayLike<T> enables
iteration over DOM collections like NamedNodeMap and NodeList:
element.attributes.forEach { attr ->
println("${attr.name}=${attr.value}")
}
element.childNodes.forEach { node ->
println(node.nodeName)
}
The test suite includes a full MVVM (Model-View-ViewModel) example demonstrating how to structure a Kotlin multiplatform application with a clear separation between platform-independent logic and browser-specific view code.
commonTest and jsTest?The ViewModel and service interfaces live in commonTest, while the View lives in jsTest. This split is intentional:
commonTest - The , service interfaces (, ), and all are pure Kotlin with no DOM dependency. They can run on any platform (JVM, Native, JS, WASM), enabling fast feedback loops during development (e.g., running tests on JVM without a browser).This separation means the bulk of your business logic and its tests remain portable and fast to execute, while only the thin view layer requires a browser.
commonTest)interface Authenticator {
suspend fun authenticate(username: String, password: String): Boolean
}
interface Navigator {
fun goTo(location: String)
}
commonTest)The LoginViewModel exposes reactive state via StateFlow and delegates side effects to injected services. The CoroutineDispatcher is injected to decouple the ViewModel from any specific threading model:
commonTest)Tests use Mokkery for mocking and kotlinx-coroutines-test for deterministic coroutine execution. No browser needed:
@Test
fun `should log in on successful authentication`() = runTest {
val dispatcher = UnconfinedTestDispatcher(testScheduler)
val authenticator = mock<Authenticator> {
everySuspend { authenticate("foo", "bar") } returns true
}
navigator = mock<Navigator>(MockMode.autoUnit)
viewModel = LoginViewModel(dispatcher, authenticator, navigator)
viewModel.onUsernameChanged()
viewModel.onPasswordChanged()
viewModel.onSubmit()
assert(!viewModel.submitEnabled.value)
assert(!viewModel.loading.value)
verifySuspend(VerifyMode.exhaustiveOrder) {
authenticator.authenticate(, )
navigator.goTo()
}
}
jsTest)The LoginView uses the DOM DSL to build HTML and binds ViewModel state flows directly to DOM properties:
fun loginView(viewModel: LoginViewModel) = nodes.form() {
aria.label =
onSubmit { it.preventDefault() }
div() {
icon()
input(, name = , type = ) {
aria.label =
onInput { viewModel.onUsernameChanged(node.value) }
}
label { + }
}
nav() {
button(, type = ) {
aria.label =
onClick { viewModel.onSubmit() }
viewModel.submitEnabled.onEach { enabled ->
node.disabled = !enabled
}.launchIn(viewModel.scope)
+
}
}
}
The view function returns a DOM node that can be appended to the document. ViewModel StateFlows are collected with onEach { ... }.launchIn(viewModel.scope) to reactively update DOM properties like disabled and hidden.
See DEVELOPMENT.md for maintenance notes — updating the gradle wrapper and the project dependencies.
jsTest - The LoginView uses the DOM DSL to build the actual HTML tree and bind it to the ViewModel. It can only run in a browser environment.| Platform | Dispatcher | Why |
|---|
| Kotlin/JS & WASM | Dispatchers.Default or Dispatchers.Main | JavaScript is single-threaded — both map to the same event-loop dispatcher. |
| Android | Dispatchers.Main (or .immediate) | StateFlow updates must be emitted on the UI thread to safely drive view updates. |
| iOS (Kotlin/Native) | Dispatchers.Main | Maps to the main dispatch queue, same reasoning as Android. |
| Tests | UnconfinedTestDispatcher | Executes coroutines eagerly and deterministically, without a real event loop. |
class LoginViewModel(
dispatcher: CoroutineDispatcher,
private val authenticator: Authenticator,
private val navigator: Navigator
) {
val submitEnabled: StateFlow<Boolean>
field = MutableStateFlow(false)
val error: StateFlow<String?>
field = MutableStateFlow<String?>(null)
val loading: StateFlow<Boolean>
field = MutableStateFlow(false)
fun onUsernameChanged(username: String) { /* ... */ }
fun onPasswordChanged(password: String) { /* ... */ }
fun onSubmit() { /* launches coroutine to authenticate */ }
fun onCleared() { scope.cancel() }
}