diff options
11 files changed, 228 insertions, 243 deletions
diff --git a/kvision-modules/kvision-redux-kotlin/src/main/kotlin/pl/treksoft/kvision/redux/ReduxStore.kt b/kvision-modules/kvision-redux-kotlin/src/main/kotlin/pl/treksoft/kvision/redux/ReduxStore.kt index 58cf86c6..a37f1ac9 100644 --- a/kvision-modules/kvision-redux-kotlin/src/main/kotlin/pl/treksoft/kvision/redux/ReduxStore.kt +++ b/kvision-modules/kvision-redux-kotlin/src/main/kotlin/pl/treksoft/kvision/redux/ReduxStore.kt @@ -28,6 +28,7 @@ import org.reduxkotlin.applyMiddleware import org.reduxkotlin.createStore import org.reduxkotlin.createThunk import org.reduxkotlin.createThunkMiddleware +import pl.treksoft.kvision.state.ObservableState interface RAction typealias ReducerFun<S, A> = (S, A) -> S @@ -65,7 +66,7 @@ class ReduxStore<S : Any, A : RAction>( reducer: ReducerFun<S, A>, initialState: S, vararg middlewares: Middleware<S> -) { +) : ObservableState<S> { @Suppress("UNCHECKED_CAST") private val store: Store<S> = createStore({ s: S, a: Any -> if (a == ActionTypes.INIT || a == ActionTypes.REPLACE) { @@ -75,11 +76,7 @@ class ReduxStore<S : Any, A : RAction>( } }, initialState, applyMiddleware(createThunkMiddleware(), *middlewares)) - /** - * Returns the current state. - */ fun getState(): S { - @Suppress("UNCHECKED_CAST") return store.getState() } @@ -99,12 +96,10 @@ class ReduxStore<S : Any, A : RAction>( store.dispatch(thunk) } - /** - * Subscribes a client for the change state notifications. - */ - fun subscribe(listener: (S) -> Unit): () -> Unit { - return store.subscribe { - listener(getState()) + override fun subscribe(observer: (S) -> Unit) { + store.subscribe { + observer(getState()) } + observer(getState()) } } diff --git a/kvision-modules/kvision-redux-kotlin/src/main/kotlin/pl/treksoft/kvision/redux/StateBinding.kt b/kvision-modules/kvision-redux-kotlin/src/main/kotlin/pl/treksoft/kvision/redux/StateBinding.kt deleted file mode 100644 index a980a79a..00000000 --- a/kvision-modules/kvision-redux-kotlin/src/main/kotlin/pl/treksoft/kvision/redux/StateBinding.kt +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (c) 2017-present Robert Jaros - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package pl.treksoft.kvision.redux - -import pl.treksoft.kvision.core.Container -import pl.treksoft.kvision.core.Widget - -/** - * A class which binds the redux store with the given container. - * - * @constructor Creates StateBinding which binds the redux store with the given container. - * @param S redux state type - * @param A redux action type - * @param CONT container type - * @param store a redux store - * @param container a container - * @param factory a function which re-creates the view based on the given state - */ -class StateBinding<S : Any, A : RAction, CONT : Container, CONTENT>( - store: ReduxStore<S, A>, - private val container: CONT, - private val factory: (CONT.(S) -> CONTENT) -) : Widget(setOf()) { - - init { - update(store.getState()) - store.subscribe { update(it) } - } - - private var updateState: ((S, CONTENT) -> Unit)? = null - private var content: CONTENT? = null - - /** - * Updates view from the current state. - */ - @Suppress("ComplexMethod") - fun update(state: S) { - singleRender { - if (updateState == null || content == null) { - container.removeAll() - container.add(this) - content = container.factory(state) - } else { - content?.let { - updateState?.invoke(state, it) - } - } - } - } - - private fun setUpdateState(updateState: (S, CONTENT) -> Unit) { - this.updateState = updateState - } - - companion object { - /** - * DSL builder extension function. - * - * It takes the same parameters as the constructor of the built component. - */ - fun <S : Any, A : RAction, CONT : Container> CONT.stateBinding( - store: ReduxStore<S, A>, - factory: (CONT.(S) -> Unit) - ): StateBinding<S, A, CONT, Unit> { - return StateBinding(store, this, factory) - } - - /** - * DSL builder extension function for updateable redux content. - * - * It takes the same parameters as the constructor of the built component. - */ - fun <S : Any, A : RAction, CONT : Container, CONTENT> CONT.stateUpdate( - store: ReduxStore<S, A>, - factory: (CONT.(S) -> CONTENT) - ): Updateable<S, CONTENT> { - return Updateable(StateBinding(store, this, factory)::setUpdateState) - } - } -} - -/** - * A helper class for updateable redux content. - */ -class Updateable<S : Any, CONTENT>(private val setUpdateState: ((S, CONTENT) -> Unit) -> Unit) { - infix fun updateWith(updateState: (S, CONTENT) -> Unit) { - setUpdateState(updateState) - } -} diff --git a/kvision-modules/kvision-redux-kotlin/src/test/kotlin/test/pl/treksoft/kvision/redux/ReduxStoreSpec.kt b/kvision-modules/kvision-redux-kotlin/src/test/kotlin/test/pl/treksoft/kvision/redux/ReduxStoreSpec.kt index a518d0ca..56195ca8 100644 --- a/kvision-modules/kvision-redux-kotlin/src/test/kotlin/test/pl/treksoft/kvision/redux/ReduxStoreSpec.kt +++ b/kvision-modules/kvision-redux-kotlin/src/test/kotlin/test/pl/treksoft/kvision/redux/ReduxStoreSpec.kt @@ -76,7 +76,7 @@ class ReduxStoreSpec : SimpleSpec { } store.dispatch(TestAction.Inc) store.dispatch(TestAction.Dec) - assertEquals(2, counter) + assertEquals(3, counter) } } } diff --git a/kvision-modules/kvision-redux-kotlin/src/test/kotlin/test/pl/treksoft/kvision/redux/StateBindingSpec.kt b/kvision-modules/kvision-redux-kotlin/src/test/kotlin/test/pl/treksoft/kvision/redux/StateBindingSpec.kt index 61845c6e..dee4daa9 100644 --- a/kvision-modules/kvision-redux-kotlin/src/test/kotlin/test/pl/treksoft/kvision/redux/StateBindingSpec.kt +++ b/kvision-modules/kvision-redux-kotlin/src/test/kotlin/test/pl/treksoft/kvision/redux/StateBindingSpec.kt @@ -25,8 +25,8 @@ import pl.treksoft.kvision.html.Div.Companion.div import pl.treksoft.kvision.panel.Root import pl.treksoft.kvision.panel.SimplePanel import pl.treksoft.kvision.redux.RAction -import pl.treksoft.kvision.redux.StateBinding.Companion.stateBinding -import pl.treksoft.kvision.redux.StateBinding.Companion.stateUpdate +import pl.treksoft.kvision.state.StateBinding.Companion.stateBinding +import pl.treksoft.kvision.state.StateBinding.Companion.stateUpdate import pl.treksoft.kvision.redux.createReduxStore import test.pl.treksoft.kvision.DomSpec import kotlin.browser.document diff --git a/kvision-modules/kvision-redux/src/main/kotlin/pl/treksoft/kvision/redux/ReduxStore.kt b/kvision-modules/kvision-redux/src/main/kotlin/pl/treksoft/kvision/redux/ReduxStore.kt index 8cb1540d..93c4f6a9 100644 --- a/kvision-modules/kvision-redux/src/main/kotlin/pl/treksoft/kvision/redux/ReduxStore.kt +++ b/kvision-modules/kvision-redux/src/main/kotlin/pl/treksoft/kvision/redux/ReduxStore.kt @@ -22,6 +22,7 @@ package pl.treksoft.kvision.redux import pl.treksoft.kvision.KVManagerRedux +import pl.treksoft.kvision.state.ObservableState import redux.Reducer import redux.Store import redux.WrapperAction @@ -62,7 +63,7 @@ class ReduxStore<S : Any, A : RAction>( reducer: Reducer<S, A>, initialState: S, vararg middlewares: dynamic -) { +) : ObservableState<S> { private val store: Store<S, dynamic, WrapperAction> init { @@ -83,9 +84,6 @@ class ReduxStore<S : Any, A : RAction>( ) } - /** - * Returns the current state. - */ fun getState(): S { return store.getState() } @@ -114,12 +112,10 @@ class ReduxStore<S : Any, A : RAction>( }) } - /** - * Subscribes a client for the change state notifications. - */ - fun subscribe(listener: (S) -> Unit): () -> Unit { - return store.subscribe { - listener(getState()) + override fun subscribe(observer: (S) -> Unit) { + store.subscribe { + observer(getState()) } + observer(getState()) } } diff --git a/kvision-modules/kvision-redux/src/main/kotlin/pl/treksoft/kvision/redux/StateBinding.kt b/kvision-modules/kvision-redux/src/main/kotlin/pl/treksoft/kvision/redux/StateBinding.kt deleted file mode 100644 index a980a79a..00000000 --- a/kvision-modules/kvision-redux/src/main/kotlin/pl/treksoft/kvision/redux/StateBinding.kt +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (c) 2017-present Robert Jaros - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package pl.treksoft.kvision.redux - -import pl.treksoft.kvision.core.Container -import pl.treksoft.kvision.core.Widget - -/** - * A class which binds the redux store with the given container. - * - * @constructor Creates StateBinding which binds the redux store with the given container. - * @param S redux state type - * @param A redux action type - * @param CONT container type - * @param store a redux store - * @param container a container - * @param factory a function which re-creates the view based on the given state - */ -class StateBinding<S : Any, A : RAction, CONT : Container, CONTENT>( - store: ReduxStore<S, A>, - private val container: CONT, - private val factory: (CONT.(S) -> CONTENT) -) : Widget(setOf()) { - - init { - update(store.getState()) - store.subscribe { update(it) } - } - - private var updateState: ((S, CONTENT) -> Unit)? = null - private var content: CONTENT? = null - - /** - * Updates view from the current state. - */ - @Suppress("ComplexMethod") - fun update(state: S) { - singleRender { - if (updateState == null || content == null) { - container.removeAll() - container.add(this) - content = container.factory(state) - } else { - content?.let { - updateState?.invoke(state, it) - } - } - } - } - - private fun setUpdateState(updateState: (S, CONTENT) -> Unit) { - this.updateState = updateState - } - - companion object { - /** - * DSL builder extension function. - * - * It takes the same parameters as the constructor of the built component. - */ - fun <S : Any, A : RAction, CONT : Container> CONT.stateBinding( - store: ReduxStore<S, A>, - factory: (CONT.(S) -> Unit) - ): StateBinding<S, A, CONT, Unit> { - return StateBinding(store, this, factory) - } - - /** - * DSL builder extension function for updateable redux content. - * - * It takes the same parameters as the constructor of the built component. - */ - fun <S : Any, A : RAction, CONT : Container, CONTENT> CONT.stateUpdate( - store: ReduxStore<S, A>, - factory: (CONT.(S) -> CONTENT) - ): Updateable<S, CONTENT> { - return Updateable(StateBinding(store, this, factory)::setUpdateState) - } - } -} - -/** - * A helper class for updateable redux content. - */ -class Updateable<S : Any, CONTENT>(private val setUpdateState: ((S, CONTENT) -> Unit) -> Unit) { - infix fun updateWith(updateState: (S, CONTENT) -> Unit) { - setUpdateState(updateState) - } -} diff --git a/kvision-modules/kvision-redux/src/test/kotlin/test/pl/treksoft/kvision/redux/ReduxStoreSpec.kt b/kvision-modules/kvision-redux/src/test/kotlin/test/pl/treksoft/kvision/redux/ReduxStoreSpec.kt index b45eb9dd..80ff65df 100644 --- a/kvision-modules/kvision-redux/src/test/kotlin/test/pl/treksoft/kvision/redux/ReduxStoreSpec.kt +++ b/kvision-modules/kvision-redux/src/test/kotlin/test/pl/treksoft/kvision/redux/ReduxStoreSpec.kt @@ -76,7 +76,7 @@ class ReduxStoreSpec : SimpleSpec { } store.dispatch(TestAction.Inc) store.dispatch(TestAction.Dec) - assertEquals(2, counter) + assertEquals(3, counter) } } } diff --git a/kvision-modules/kvision-redux/src/test/kotlin/test/pl/treksoft/kvision/redux/StateBindingSpec.kt b/kvision-modules/kvision-redux/src/test/kotlin/test/pl/treksoft/kvision/redux/StateBindingSpec.kt index ec32d0d6..005a2da3 100644 --- a/kvision-modules/kvision-redux/src/test/kotlin/test/pl/treksoft/kvision/redux/StateBindingSpec.kt +++ b/kvision-modules/kvision-redux/src/test/kotlin/test/pl/treksoft/kvision/redux/StateBindingSpec.kt @@ -24,8 +24,8 @@ package test.pl.treksoft.kvision.redux import pl.treksoft.kvision.html.Div.Companion.div import pl.treksoft.kvision.panel.Root import pl.treksoft.kvision.panel.SimplePanel -import pl.treksoft.kvision.redux.StateBinding.Companion.stateBinding -import pl.treksoft.kvision.redux.StateBinding.Companion.stateUpdate +import pl.treksoft.kvision.state.StateBinding.Companion.stateBinding +import pl.treksoft.kvision.state.StateBinding.Companion.stateUpdate import pl.treksoft.kvision.redux.createReduxStore import redux.RAction import test.pl.treksoft.kvision.DomSpec diff --git a/src/main/kotlin/pl/treksoft/kvision/state/ObservableState.kt b/src/main/kotlin/pl/treksoft/kvision/state/ObservableState.kt new file mode 100644 index 00000000..9dc9af0d --- /dev/null +++ b/src/main/kotlin/pl/treksoft/kvision/state/ObservableState.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package pl.treksoft.kvision.state + +/** + * An interface of observable state. + */ +interface ObservableState<S> { + /** + * Subscribe for the state change notifications. + */ + fun subscribe(observer: (S) -> Unit) +} diff --git a/src/main/kotlin/pl/treksoft/kvision/state/StateBinding.kt b/src/main/kotlin/pl/treksoft/kvision/state/StateBinding.kt new file mode 100644 index 00000000..7df82adc --- /dev/null +++ b/src/main/kotlin/pl/treksoft/kvision/state/StateBinding.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2019. Robert Jaros + */ +package pl.treksoft.kvision.state + +import pl.treksoft.kvision.core.Container +import pl.treksoft.kvision.core.Widget + +/** + * A class which binds the given container to the observable state. + * + * @constructor Creates StateBinding which binds the given container to the observable state. + * @param S the state type + * @param CONT container type + * @param observableState the state + * @param container the container + * @param factory a function which re-creates the view based on the given state + */ +class StateBinding<S : Any, CONT : Container, CONTENT>( + observableState: ObservableState<S>, + private val container: CONT, + private val factory: (CONT.(S) -> CONTENT) +) : Widget(setOf()) { + + init { + observableState.subscribe { update(it) } + } + + private var updateState: ((S, CONTENT) -> Unit)? = null + private var content: CONTENT? = null + + /** + * Updates view based on the current state. + */ + @Suppress("ComplexMethod") + fun update(state: S) { + singleRender { + if (updateState == null || content == null) { + container.removeAll() + container.add(this) + content = container.factory(state) + } else { + content?.let { + updateState?.invoke(state, it) + } + } + } + } + + private fun setUpdateState(updateState: (S, CONTENT) -> Unit) { + this.updateState = updateState + } + + companion object { + /** + * DSL builder extension function. + * + * It takes the same parameters as the constructor of the built component. + */ + fun <S : Any, CONT : Container> CONT.stateBinding( + observableState: ObservableState<S>, + factory: (CONT.(S) -> Unit) + ): StateBinding<S, CONT, Unit> { + return StateBinding(observableState, this, factory) + } + + /** + * DSL builder extension function for updateable redux content. + * + * It takes the same parameters as the constructor of the built component. + */ + fun <S : Any, CONT : Container, CONTENT> CONT.stateUpdate( + observableState: ObservableState<S>, + factory: (CONT.(S) -> CONTENT) + ): Updateable<S, CONTENT> { + return Updateable(StateBinding(observableState, this, factory)::setUpdateState) + } + } +} + +/** + * A helper class for updateable content. + */ +class Updateable<S : Any, CONTENT>(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 new file mode 100644 index 00000000..f5416748 --- /dev/null +++ b/src/test/kotlin/test/pl/treksoft/kvision/state/StateBindingSpec.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package test.pl.treksoft.kvision.state + +import pl.treksoft.kvision.html.Div.Companion.div +import pl.treksoft.kvision.panel.Root +import pl.treksoft.kvision.panel.SimplePanel +import pl.treksoft.kvision.state.StateBinding.Companion.stateBinding +import pl.treksoft.kvision.state.StateBinding.Companion.stateUpdate +import pl.treksoft.kvision.state.observableListOf +import test.pl.treksoft.kvision.DomSpec +import kotlin.browser.document +import kotlin.test.Test + +class StateBindingSpec : DomSpec { + + @Test + fun stateBinding() { + run { + val root = Root("test", fixed = true) + val container = SimplePanel() + val observableList = observableListOf(1, 2, 3) + container.stateBinding(observableList) { state -> + state.forEach { + div("$it") + } + } + root.add(container) + val element = document.getElementById("test") + assertEqualsHtml( + "<div><div></div><div>1</div><div>2</div><div>3</div></div>", + element?.innerHTML, + "Should render initial state of the container" + ) + observableList.add(4) + assertEqualsHtml( + "<div><div></div><div>1</div><div>2</div><div>3</div><div>4</div></div>", + element?.innerHTML, + "Should render changed state of the container" + ) + } + } + + @Test + fun stateUpdate() { + run { + val root = Root("test", fixed = true) + val container = SimplePanel() + val observableList = observableListOf(1) + container.stateUpdate(observableList) { state -> + div("${state[0]}") + } updateWith { state, d -> + d.content = "${state[0]}" + } + root.add(container) + val element = document.getElementById("test") + assertEqualsHtml( + "<div><div></div><div>1</div></div>", + element?.innerHTML, + "Should render initial state of the container" + ) + observableList[0] = 2 + assertEqualsHtml( + "<div><div></div><div>2</div></div>", + element?.innerHTML, + "Should render changed state of the container" + ) + } + } + +} |