Components

Managing Complexity

Composable components help manage software complexity by allowing developers to break down a complex problem into smaller, more manageable pieces. Other benefits include reusability, testability, and the ability to reason about a system in isolation.

The Component typealias

We rely on a small amount of syntactic sugar defined in kweb.components.Component:

typealias Component = ElementCreator<*>

And then we can use an extension function on this to create a component:

fun Component.simple(
    prompt: String = "Enter Your Name",
    name: KVar<String>
) {
    div {
        h1().text(prompt)
        input(type = text).value = name
    }
    div {
        span().text(name.map { "Hello, $it" })
    }
}

And we use the component like this:

    Kweb(port = 16097) {
        doc.body {
            simple(name = kvar("World"))
        }
    }

Components are configured through constructor parameters, typically through a mixture of:

  • KVals for values that can change
  • KVars for values that can change or be changed by the Component
  • ObservableLists for lists of values that can change
  • Other normal classes like String for immutable values

The simplest Component's may have no constructor at all, or just one or two, parameters, while the most complex might use a DSL builder.

Component's can be rendered by calling their render() method, which can return a generically typed value (or Unit if no value is returned).

A more complex example

In this example we create a Component that wraps an <input> element styled using the Bulma CSS framework:

enum class BulmaColor(val cssClassName: String) {
    PRIMARY("is-primary"),
    LINK("is-link"),
    INFO("is-info"),
    SUCCESS("is-success"),
    WARNING("is-warning"),
    DANGER("is-danger")
}

enum class BulmaSize(val cssClassName: String) {
    SMALL("is-small"),
    NORMAL("is-normal"),
    MEDIUM("is-medium"),
    LARGE("is-large")
}

enum class BulmaStyle(val cssClassName: String) {
    ROUNDED("is-rounded"),
    FOCUSED("is-focused")
}

enum class BulmaState(val cssClassName: String) {
    NORMAL("is-normal"),
    HOVER("is-hovered"),
    FOCUS("is-focused"),
    LOADING("is-loading"),
}

fun Component.bulmaInput(
    type: InputType,
    color: KVal<BulmaColor>? = null,
    size: KVal<BulmaSize>? = null,
    style: KVal<BulmaStyle>? = null,
    state: KVal<BulmaState>? = null,
    disabled: KVal<Boolean>? = null,
    value: KVar<String>
) {
    input(type = type) { inputElement ->
        var inputClassList: KVal<List<String>> = kval(listOf("input"))
        with(inputElement) {

            if (color != null) {
                inputClassList += color.map { listOf(it.cssClassName) }
            }
            if (size != null) {
                inputClassList += size.map { listOf(it.cssClassName) }
            }
            if (style != null) {
                inputClassList += style.map { listOf(it.cssClassName) }
            }
            if (state != null) {
                inputClassList += state.map { listOf(it.cssClassName) }
            }

            if (disabled != null) {
                this["disabled"] = disabled.map { it.json }
            }

            classes(inputClassList.map { it.joinToString(" ") })

            this.value = value

        }
    }
}

This component can then be used like this:

    Kweb(port = 12354) {
        doc.head {
            element(
                "link",
                attributes = mapOf(
                    "rel" to "stylesheet".json,
                    "href" to "https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css".json
                )
            )
        }

        doc.body {
            val username = kvar("")
            val color = username.map { if (it.length < 5) BulmaColor.WARNING else BulmaColor.SUCCESS }
            bulmaInput(type = text, value = username, color = color)
        }
    }