Skip to content
This repository has been archived by the owner on Oct 9, 2022. It is now read-only.

Latest commit

 

History

History
195 lines (134 loc) · 6.89 KB

state.rst

File metadata and controls

195 lines (134 loc) · 6.89 KB

Observer Pattern & State

Overview

Kweb uses the observer pattern to manage state.

A Kweb app can be viewed as a mapping function between state on the server and the DOM within the end-user's web browser. Once this mapping is defined, simply modify this state and the change will propagate automatically to the browser.

Building blocks

A KVar class contains a single typed object, 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}")
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.

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

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. 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 clickMe = input(type = text)
        val nameKVar = KVar("Peter Pan")
        clickMe.value = nameKVar
        p().text(nameKVar.map { n -> "Hi $n!" })
    }
}

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.

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. KVal's values may not be modified directly, so this won't be permitted:

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

Note

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 will be what you need.