From 0b349de3cc699b54b02d7177418c8cf155afe929 Mon Sep 17 00:00:00 2001 From: Robert Jaros Date: Sun, 3 May 2020 22:43:53 +0200 Subject: Simplify state binding functions. Deprecate stateBinding() in favor of bind(). --- src/main/kotlin/pl/treksoft/kvision/core/Widget.kt | 37 ++++++++++++++++++++++ .../pl/treksoft/kvision/state/StateBinding.kt | 25 +++++++++++++++ .../pl/treksoft/kvision/state/StateBindingSpec.kt | 31 ++++++++++++++++++ 3 files changed, 93 insertions(+) (limited to 'src') diff --git a/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt b/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt index a1d37541..2d2fac29 100644 --- a/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt +++ b/src/main/kotlin/pl/treksoft/kvision/core/Widget.kt @@ -33,6 +33,7 @@ import pl.treksoft.kvision.KVManager import pl.treksoft.kvision.i18n.I18n import pl.treksoft.kvision.i18n.I18n.trans import pl.treksoft.kvision.panel.Root +import pl.treksoft.kvision.state.ObservableState import pl.treksoft.kvision.utils.SnOn import pl.treksoft.kvision.utils.emptyOn import pl.treksoft.kvision.utils.hooks @@ -70,22 +71,27 @@ open class Widget(classes: Set = setOf()) : StyledComponent(), Component field = value if (oldField != field) refresh() } + /** * A title attribute of generated HTML element. */ var title: String? by refreshOnUpdate() + /** * An ID attribute of generated HTML element. */ var id: String? by refreshOnUpdate() + /** * A role attribute of generated HTML element. */ var role: String? by refreshOnUpdate() + /** * A tabindex attribute of generated HTML element. */ var tabindex: Int? by refreshOnUpdate() + /** * Determines if the current widget is draggable. */ @@ -111,8 +117,18 @@ open class Widget(classes: Set = setOf()) : StyledComponent(), Component protected var lastLanguage: String? = null + /** + * A function called after the widget is inserted to the DOM. + */ var afterInsertHook: ((VNode) -> Unit)? = null + /** + * A function called after the widget is removed from the DOM. + */ var afterDestroyHook: (() -> Unit)? = null + /** + * A function called after the widget is disposed. + */ + var afterDisposeHook: (() -> Unit)? = null protected fun singleRender(block: () -> T): T { getRoot()?.renderDisabled = true @@ -758,6 +774,7 @@ open class Widget(classes: Set = setOf()) : StyledComponent(), Component } override fun dispose() { + afterDisposeHook?.invoke() } protected fun refreshOnUpdate(refreshFunction: ((T) -> Unit) = { this.refresh() }): RefreshDelegateProvider = @@ -797,6 +814,26 @@ open class Widget(classes: Set = setOf()) : StyledComponent(), Component companion object { private var counter: Long = 0 + + /** + * An extension function which binds the widget to the observable state. + * Used by [pl.treksoft.kvision.state.bind] + */ + internal fun W.bindState( + observableState: ObservableState, + factory: (W.(S) -> Unit) + ): W { + val unsubscribe = observableState.subscribe { + this.singleRender { + (this as? Container)?.removeAll() + this.factory(it) + } + } + this.afterDisposeHook = { + unsubscribe() + } + return this + } } } diff --git a/src/main/kotlin/pl/treksoft/kvision/state/StateBinding.kt b/src/main/kotlin/pl/treksoft/kvision/state/StateBinding.kt index 20cb2d8e..d330f76d 100644 --- a/src/main/kotlin/pl/treksoft/kvision/state/StateBinding.kt +++ b/src/main/kotlin/pl/treksoft/kvision/state/StateBinding.kt @@ -5,6 +5,22 @@ package pl.treksoft.kvision.state import pl.treksoft.kvision.core.Container import pl.treksoft.kvision.core.Widget +import pl.treksoft.kvision.core.Widget.Companion.bindState + +/** + * An extension function which binds the widget to the observable state. + * + * @param S the state type + * @param W the widget type + * @param observableState the state + * @param factory a function which re-creates the view based on the given state + */ +fun W.bind( + observableState: ObservableState, + factory: (W.(S) -> Unit) +): W { + return this.bindState(observableState, factory) +} /** * A class which binds the given container to the observable state. @@ -16,6 +32,7 @@ import pl.treksoft.kvision.core.Widget * @param container the container * @param factory a function which re-creates the view based on the given state */ +@Deprecated("Use bind function instead.") class StateBinding( observableState: ObservableState, private val container: CONT, @@ -64,6 +81,11 @@ class StateBinding( * * It takes the same parameters as the constructor of the built component. */ +@Suppress("DEPRECATION") +@Deprecated( + "Use bind function instead.", + replaceWith = ReplaceWith("bind(observableState, factory)", "pl.treksoft.kvision.state.bind") +) fun CONT.stateBinding( observableState: ObservableState, factory: (CONT.(S) -> Unit) @@ -76,6 +98,8 @@ fun CONT.stateBinding( * * It takes the same parameters as the constructor of the built component. */ +@Suppress("DEPRECATION") +@Deprecated("Use bind function instead.") fun CONT.stateUpdate( observableState: ObservableState, factory: (CONT.(S) -> CONTENT) @@ -86,6 +110,7 @@ fun CONT.stateUpdate( /** * A helper class for updateable content. */ +@Deprecated("Use bind function instead.") class Updateable(private val setUpdateState: ((S, CONTENT) -> Unit) -> Unit) { infix fun updateWith(updateState: (S, CONTENT) -> Unit) { setUpdateState(updateState) diff --git a/src/test/kotlin/test/pl/treksoft/kvision/state/StateBindingSpec.kt b/src/test/kotlin/test/pl/treksoft/kvision/state/StateBindingSpec.kt index c0de7c6f..940fd2cd 100644 --- a/src/test/kotlin/test/pl/treksoft/kvision/state/StateBindingSpec.kt +++ b/src/test/kotlin/test/pl/treksoft/kvision/state/StateBindingSpec.kt @@ -24,6 +24,7 @@ package test.pl.treksoft.kvision.state import pl.treksoft.kvision.html.div import pl.treksoft.kvision.panel.Root import pl.treksoft.kvision.panel.SimplePanel +import pl.treksoft.kvision.state.bind import pl.treksoft.kvision.state.observableListOf import pl.treksoft.kvision.state.stateBinding import pl.treksoft.kvision.state.stateUpdate @@ -33,6 +34,35 @@ import kotlin.test.Test class StateBindingSpec : DomSpec { + @Test + fun bind() { + run { + val root = Root("test", fixed = true) + val container = SimplePanel() + val observableList = observableListOf(1, 2, 3) + container.bind(observableList) { state -> + setAttribute("data-count", "${state.size}") + state.forEach { + div("$it") + } + } + root.add(container) + val element = document.getElementById("test") + assertEqualsHtml( + "
1
2
3
", + element?.innerHTML, + "Should render initial state of the widget" + ) + observableList.add(4) + assertEqualsHtml( + "
1
2
3
4
", + element?.innerHTML, + "Should render changed state of the widget" + ) + } + } + + @Suppress("DEPRECATION") @Test fun stateBinding() { run { @@ -60,6 +90,7 @@ class StateBindingSpec : DomSpec { } } + @Suppress("DEPRECATION") @Test fun stateUpdate() { run { -- cgit