Rendering State

Overview

A Kweb app is a function that maps state on the server to the DOM in the user's web browser. Once this mapping is defined, you can change the state and the browser will update automatically.

The KVar Class

A KVar is an observable container. It contains a single typed object, which can change over time, similar to an AtomicReference. You can add listeners to a KVar to be notified immediately when it changes.

For example:

val counter = KVar(0)

Here we create a counter of type KVar<Int> initialized with the value 0.

We can also read and modify the value of a KVar:

println("Counter value ${counter.value}")
counter.value = 1
println("Counter value ${counter.value}")
counter.value++
println("Counter value ${counter.value}")

Will print:

Counter value 0
Counter value 1
Counter value 2

KVars support powerful mapping semantics to create new KVars:

val counterDoubled = counter.map { it * 2 }
counter.value = 5
println("counter: ${counter.value}, doubled: ${counterDoubled.value}")
counter.value = 6
println("counter: ${counter.value}, doubled: ${counterDoubled.value}")

Will print:

counter: 5, doubled: 10
counter: 6, doubled: 12

Note that counterDoubled updates automatically, because mapped KVars listen to the original for changes.

The KVar class is a subclass of KVal, which is a read-only version of KVar.

Note: KVars should only be used to store values that are themselves immutable, such as an Int, String, or a Kotlin data class with immutable parameters.

KVars and the DOM

Within the Kotlin DSL you can use the kvar() function to create a KVar. This has the advantage of calling KVar.close() when this DOM fragment is cleaned up which will free up resources, and avoids having to import the KVar class.

Kweb(port = 2135) {
    doc.body {
        val name = kvar("John")
        li().text(name)
    }
}

The neat part is that if the value of name changes, the DOM element text will update automatically. It may help to think of this as a way of "unwrapping" a KVar.

Numerous other functions on Elements support KVars in a similar manner, including innerHtml() and setAttribute().

Binding a KVar to an input element's value

For <input> elements you can set the value to a KVar, which will connect them bidirectionally.

Any changes to the KVar will be reflected in realtime in the browser, and similarly any changes in the browser by the user will be reflected immediately in the KVar, for example:

Kweb(port = 2395) {
    doc.body {
        p().text("What is your name?")
        val input = input(type = InputType.text)
        input.value = KVar("Peter Pan")
        val greeting = input.value.map { name -> "Hi $name!" }
        p().text(greeting)
    }
}

This will also work for <option> and <textarea> elements which also have values.

See also: ValueElement.value

Rendering state to a DOM fragment

But what if you want to do more than just modify a single element based on a KVar, what if you want to modify a whole tree of elements?

This is where the render

function comes in:

val list = KVar(listOf("one", "two", "three"))

Kweb(port = 16097) {
    doc.body {
        render(list) { rList ->
            ul {
                for (item in rList) {
                    li().text(item)
                }
            }
        }
    }
}

Here, if we were to change the list:

list.value = listOf("four", "five", "six")

Then the relevant part of the DOM will be redrawn instantly.

The simplicity of this mechanism may disguise how powerful it is, since render {} blocks can be nested, it's possible to be very selective about what parts of the DOM must be modified in response to changes in state.

Note: Kweb will only re-render a DOM fragment if the value of the KVar actually changes so you should avoid "unwrapping" KVars with a render() or .text() call before you need to.

The KVal.map {} function is a powerful tool for manipulating KVals and KVars without unwrapping them.

Rendering lists with renderEach

The renderEach() function allows you to render a list of items, while automatically updating the rendered DOM in response to changes in the list.

While a KVar<List<FooBar>> could be passed to render(), it would be very inefficient because the entire list would be re-rendered every time. renderEach() will only re-render the elements that have changed.

The items are provided in an ObservableList, which implements the MutableList interface.

doc.body {
    data class Pet(val name : String, val age : Int)

    val obsList = ObservableList(listOf(
        Pet("Sammy", 7),
        Pet("Halley", 5),
        Pet("Buddy", 3)
    ))
    table {
        renderEach(obsList) { item ->
            tr {
                td().text(item.name)
                td().text(item.age.toString())
            }
        }
    }
    obsList.add(1, Pet("Bella", 2))
    obsList.removeAt(2)
    obsList.move(0, 1)
    obsList[0] = Pet("Joe", 1)
}

Extracting data class properties

If your KVar contains a data class then you can use Kvar.property() to create a KVar from one of its properties which will update the original KVar if changed:

data class User(val name: String)

val user = KVar(User("Ian"))
val name = user.property(User::name)
name.value = "John"
println(user) // Will print: KVar(User(name = "John"))

Reversible mapping

If you check the type of counterDoubled, you'll notice that it's a KVal rather than a KVar, meaning it cannot be modified directly, only by changing the KVar it was mapped from.

So this will result in a compilation error:

val counter = KVar(0)
val counterDoubled = counter.map { it * 2 }
counterDoubled.value = 20 // <--- This won't compile

The KVar class has a second map() function which takes a ReversibleFunction implementation. This version of map will produce a KVar which can be modified, as follows:

val counterDoubled = counter.map(object : ReversibleFunction<Int, Int>("doubledCounter") {
    override fun invoke(from: Int) = from * 2
    override fun reverse(original: Int, change: Int) = change / 2
})
counter.value = 5
println("counter: ${counter.value}, doubled: ${counterDoubled.value}")
// output: counter: 5, doubled: 10

counterDoubled.value = 12 // <-- Couldn't do this with a KVal
println("counter: ${counter.value}, doubled: ${counterDoubled.value}")
// output: counter: 6, doubled: 12

If the mapped Kvar is changed the original KVar it was mapped from will also change.

Reversible mappings are an advanced feature that you only need if you want the mapped value to be a mutable KVar. Most of the time the simple KVal.map {} function is what you need.