State Management

Building Blocks

Kweb makes use of the observer pattern, through the KVar class.

A KVar can contain a value of any type, which can change over time, 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}")

Will print:

Counter value 0
counter value 1

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 how counterDoubled updates automatically.

KVars and the DOM

You can use a KVar (or KVal) to set the text of a DOM element:

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.

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

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 = 1234) {
    doc.body.new {
        render(list) { rList ->
            ul().new {
                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

Routing with KVars

You can obtain and modify the URL of the current page using url(simpleUrlParser). This returns a KVar<URL >, which you can then use with render to handle however you wish.

Note

You can modify this KVar<URL> and the browser URL bar and DOM will update accordingly, but without a page refresh.

Kweb(port = 1234) {
    doc.body.new {
        val url: KVar<URL> = url(simpleUrlParser)
        render(url.path) { path ->
            ul().new {
                for (p in path) {
                    li().text(p)
                }
            }
        }
    }
}

And that’s pretty-much all you need to know to handle URL routing in your app, although we will make more specific recommendations later.

KVars and Persistent Storage

While you don’t have to use it, Kweb integrates nicely with Shoebox, a key-value store that supports the observer pattern. Shoebox has both in-memory and persistent (on disk) engines, and new engines can be added quite easily.

We’ll assume you’ve taken a few minutes to review Shoebox and get the general idea of how it’s used.

This example shows how toVar can be used to convert a value in a Shoebox to a KVar, and use it with the DOM as previously described:

fun main() {
    data class User(val name : String, val email : String)
    val users = Shoebox<User>()
    users["aaa"] = User("Ian", "[email protected]")

    Kweb(port = 1234) {
        doc.body.new {
            val user = toVar(users, "aaa")
            ul().new {
                li().text(user.map {"Name: ${it.name}"})
                li().text(user.map {"Email: ${it.email}"})

            }
        }
    }
}

Reversible mappings

If you check the type of counterDoubled, you’ll notice that it’s a KVal rather than a KVar. KVal’s values may not be modified directly, so this won’t be permitted:

counterDoubled.value = 20 // <--- This won't compile

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

val counterDoubled = counter.map(object : ReversableFunction<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}")

counterDoubled.value = 12 // <--- This wouldn't have worked before
println("counter: ${counter.value}, doubled: ${counterDoubled.value}")

Will print:

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