diff options
author | Robert Jaros <rjaros@finn.pl> | 2019-03-14 15:22:23 +0100 |
---|---|---|
committer | Robert Jaros <rjaros@finn.pl> | 2019-03-14 15:22:23 +0100 |
commit | cb27ed25b9e5a448a3de95be41733e90d75d5933 (patch) | |
tree | 5b365218e83cb88be961ddf7af1a8d0b362aaa05 /kvision-modules/kvision-datacontainer | |
parent | 56f45a8043c24491da9990e10d1cdc9cc22d4eca (diff) | |
download | kvision-cb27ed25b9e5a448a3de95be41733e90d75d5933.tar.gz kvision-cb27ed25b9e5a448a3de95be41733e90d75d5933.tar.bz2 kvision-cb27ed25b9e5a448a3de95be41733e90d75d5933.zip |
Refactor DataContainer component to the separate module.
Diffstat (limited to 'kvision-modules/kvision-datacontainer')
7 files changed, 485 insertions, 0 deletions
diff --git a/kvision-modules/kvision-datacontainer/build.gradle b/kvision-modules/kvision-datacontainer/build.gradle new file mode 100644 index 00000000..7ad09e67 --- /dev/null +++ b/kvision-modules/kvision-datacontainer/build.gradle @@ -0,0 +1,15 @@ +apply from: "../shared.gradle" + +dependencies { + compile "pl.treksoft:kotlin-observable-js:$kotlinObservableVersion" +} + +kotlinFrontend { + + npm { + devDependency("karma", "3.1.4") + devDependency("karma-chrome-launcher", "2.2.0") + devDependency("qunit", "2.8.0") + } + +} diff --git a/kvision-modules/kvision-datacontainer/package.json.d/project.info b/kvision-modules/kvision-datacontainer/package.json.d/project.info new file mode 100644 index 00000000..86ea568f --- /dev/null +++ b/kvision-modules/kvision-datacontainer/package.json.d/project.info @@ -0,0 +1,3 @@ +{ + "description": "KVision DataContainer module" +} diff --git a/kvision-modules/kvision-datacontainer/src/main/kotlin/pl/treksoft/kvision/data/DataComponent.kt b/kvision-modules/kvision-datacontainer/src/main/kotlin/pl/treksoft/kvision/data/DataComponent.kt new file mode 100644 index 00000000..4e5b92d5 --- /dev/null +++ b/kvision-modules/kvision-datacontainer/src/main/kotlin/pl/treksoft/kvision/data/DataComponent.kt @@ -0,0 +1,54 @@ +/* + * 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.data + +import kotlin.properties.ObservableProperty +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +/** + * Base interface for observable data model. + */ +interface DataComponent { + /** + * @suppress + * Internal property + */ + var container: DataUpdatable? + + /** + * @suppress + * Internal function for observable properties + */ + fun <T> obs(initialValue: T): ReadWriteProperty<Any?, T> = object : ObservableProperty<T>(initialValue) { + override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) { + container?.update() + } + } +} + +/** + * Base abstract class for creating observable data model. + */ +abstract class BaseDataComponent : DataComponent { + override var container: DataUpdatable? = null +} diff --git a/kvision-modules/kvision-datacontainer/src/main/kotlin/pl/treksoft/kvision/data/DataContainer.kt b/kvision-modules/kvision-datacontainer/src/main/kotlin/pl/treksoft/kvision/data/DataContainer.kt new file mode 100644 index 00000000..69851b29 --- /dev/null +++ b/kvision-modules/kvision-datacontainer/src/main/kotlin/pl/treksoft/kvision/data/DataContainer.kt @@ -0,0 +1,216 @@ +/* + * 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.data + +import com.github.snabbdom.VNode +import com.lightningkite.kotlin.observable.list.ObservableList +import pl.treksoft.kvision.core.Component +import pl.treksoft.kvision.core.Container +import pl.treksoft.kvision.core.Widget +import pl.treksoft.kvision.panel.VPanel + +/** + * Sorter types. + */ +enum class SorterType { + ASC, + DESC +} + +/** + * A container class with support for observable data model. + * + * @constructor Creates DataContainer bound to given data model. + * @param M data model type + * @param C visual component type + * @param CONT container type + * @param model data model of type *ObservableList<M>* + * @param factory a function which creates component C from data model at given index + * @param container internal container + * @param containerAdd function to add component C to the internal container CONT + * @param filter a filtering function + * @param sorter a sorting function + * @param sorterType a sorting type selection function + * @param init an initializer extension function + */ +class DataContainer<M, C : Component, CONT : Container>( + private val model: ObservableList<M>, + private val factory: (M, Int, ObservableList<M>) -> C, + private val container: CONT, + private val containerAdd: (CONT.(C, M) -> Unit)? = null, + private val filter: ((M) -> Boolean)? = null, + private val sorter: ((M) -> Comparable<*>?)? = null, + private val sorterType: () -> SorterType = { SorterType.ASC }, + init: (DataContainer<M, C, CONT>.() -> Unit)? = null +) : + Widget(setOf()), Container, DataUpdatable { + + override var visible + get() = container.visible + set(value) { + container.visible = value + } + + internal var onUpdateHandler: (() -> Unit)? = null + + init { + container.parent = this + model.onUpdate += { + update() + } + update() + @Suppress("LeakingThis") + init?.invoke(this) + } + + override fun add(child: Component): Container { + this.container.add(child) + return this + } + + override fun addAll(children: List<Component>): Container { + this.container.addAll(children) + return this + } + + override fun remove(child: Component): Container { + this.container.remove(child) + return this + } + + override fun removeAll(): Container { + this.container.removeAll() + return this + } + + override fun getChildren(): List<Component> { + return this.container.getChildren() + } + + override fun renderVNode(): VNode { + return this.container.renderVNode() + } + + /** + * Updates view from the current data model state. + */ + @Suppress("ComplexMethod") + override fun update() { + model.forEach { + if (it is DataComponent) it.container = this + } + singleRender { + container.removeAll() + val indexed = model.mapIndexed { index, m -> m to index } + val sorted = if (sorter != null) { + when (sorterType()) { + SorterType.ASC -> + indexed.sortedBy { + @Suppress("UNCHECKED_CAST") + sorter.invoke(it.first) as Comparable<Any>? + } + SorterType.DESC -> + indexed.sortedByDescending { + @Suppress("UNCHECKED_CAST") + sorter.invoke(it.first) as Comparable<Any>? + } + } + } else { + indexed + } + val filtered = if (filter != null) { + sorted.filter { filter.invoke(it.first) } + } else { + sorted + } + val children = filtered.map { p -> p.first to factory(p.first, p.second, model) } + if (containerAdd != null) { + children.forEach { child -> + containerAdd.invoke(container, child.second, child.first) + } + } else { + container.addAll(children.map { it.second }) + } + } + onUpdateHandler?.invoke() + } + + /** + * Sets a notification handler called after every update. + * @param handler notification handler + * @return current container + */ + fun onUpdate(handler: () -> Unit): DataContainer<M, C, CONT> { + onUpdateHandler = handler + return this + } + + /** + * Clears notification handler. + * @return current container + */ + fun clearOnUpdate(): DataContainer<M, C, CONT> { + onUpdateHandler = null + return this + } + + companion object { + /** + * DSL builder extension function. + * + * It takes the same parameters as the constructor of the built component. + */ + fun <M, C : Component, CONT : Container> Container.dataContainer( + model: ObservableList<M>, + factory: (M, Int, ObservableList<M>) -> C, + container: CONT, + containerAdd: (CONT.(C, M) -> Unit)? = null, + filter: ((M) -> Boolean)? = null, + sorter: ((M) -> Comparable<*>?)? = null, + sorterType: () -> SorterType = { SorterType.ASC }, + init: (DataContainer<M, C, CONT>.() -> Unit)? = null + ): DataContainer<M, C, CONT> { + val dataContainer = DataContainer(model, factory, container, containerAdd, filter, sorter, sorterType, init) + this.add(dataContainer) + return dataContainer + } + + /** + * DSL builder extension function with VPanel default. + * + * It takes the same parameters as the constructor of the built component. + */ + fun <M, C : Component> Container.dataContainer( + model: ObservableList<M>, + factory: (M, Int, ObservableList<M>) -> C, + containerAdd: (VPanel.(C, M) -> Unit)? = null, + filter: ((M) -> Boolean)? = null, + sorter: ((M) -> Comparable<*>?)? = null, + sorterType: () -> SorterType = { SorterType.ASC }, + init: (DataContainer<M, C, VPanel>.() -> Unit)? = null + ): DataContainer<M, C, VPanel> { + val dataContainer = DataContainer(model, factory, VPanel(), containerAdd, filter, sorter, sorterType, init) + this.add(dataContainer) + return dataContainer + } + } +} diff --git a/kvision-modules/kvision-datacontainer/src/main/kotlin/pl/treksoft/kvision/data/DataUpdatable.kt b/kvision-modules/kvision-datacontainer/src/main/kotlin/pl/treksoft/kvision/data/DataUpdatable.kt new file mode 100644 index 00000000..7c54e864 --- /dev/null +++ b/kvision-modules/kvision-datacontainer/src/main/kotlin/pl/treksoft/kvision/data/DataUpdatable.kt @@ -0,0 +1,29 @@ +/* + * 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.data + +/** + * Interface for updatable container. + */ +interface DataUpdatable { + fun update() +} diff --git a/kvision-modules/kvision-datacontainer/src/test/kotlin/test/pl/treksoft/kvision/TestUtil.kt b/kvision-modules/kvision-datacontainer/src/test/kotlin/test/pl/treksoft/kvision/TestUtil.kt new file mode 100644 index 00000000..9d86766c --- /dev/null +++ b/kvision-modules/kvision-datacontainer/src/test/kotlin/test/pl/treksoft/kvision/TestUtil.kt @@ -0,0 +1,99 @@ +/* + * 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 + +import org.w3c.dom.Element +import pl.treksoft.jquery.jQuery +import pl.treksoft.kvision.core.Widget +import pl.treksoft.kvision.panel.Root +import kotlin.browser.document +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +interface TestSpec { + fun beforeTest() + + fun afterTest() + + fun run(code: () -> Unit) { + beforeTest() + code() + afterTest() + } +} + +interface SimpleSpec : TestSpec { + + override fun beforeTest() { + } + + override fun afterTest() { + } + +} + +interface DomSpec : TestSpec { + + override fun beforeTest() { + val fixture = "<div style=\"display: none\" id=\"pretest\">" + + "<div id=\"test\"></div></div>" + document.body?.insertAdjacentHTML("afterbegin", fixture) + } + + override fun afterTest() { + val div = document.getElementById("pretest") + div?.let { jQuery(it).remove() } + jQuery(".modal-backdrop").remove() + } + + fun assertEqualsHtml(expected: String?, actual: String?, message: String?) { + if (expected != null && actual != null) { + val exp = jQuery(expected) + val act = jQuery(actual) + val result = exp[0]?.isEqualNode(act[0]) + if (result == true) { + assertTrue(result == true, message) + } else { + assertEquals(expected, actual, message) + } + } else { + assertEquals(expected, actual, message) + } + } +} + +interface WSpec : DomSpec { + + fun runW(code: (widget: Widget, element: Element?) -> Unit) { + run { + val root = Root("test", true) + val widget = Widget() + widget.id = "test_id" + root.add(widget) + val element = document.getElementById("test_id") + code(widget, element) + } + } + +} + +external fun require(name: String): dynamic diff --git a/kvision-modules/kvision-datacontainer/src/test/kotlin/test/pl/treksoft/kvision/data/DataContainerSpec.kt b/kvision-modules/kvision-datacontainer/src/test/kotlin/test/pl/treksoft/kvision/data/DataContainerSpec.kt new file mode 100644 index 00000000..931294d5 --- /dev/null +++ b/kvision-modules/kvision-datacontainer/src/test/kotlin/test/pl/treksoft/kvision/data/DataContainerSpec.kt @@ -0,0 +1,69 @@ +/* + * 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.data + +import com.lightningkite.kotlin.observable.list.observableListOf +import pl.treksoft.kvision.data.BaseDataComponent +import pl.treksoft.kvision.data.DataContainer +import pl.treksoft.kvision.html.Label +import pl.treksoft.kvision.panel.Root +import pl.treksoft.kvision.panel.VPanel +import test.pl.treksoft.kvision.DomSpec +import kotlin.browser.document +import kotlin.test.Test + +class DataContainerSpec : DomSpec { + + @Test + fun render() { + run { + val root = Root("test", true) + + class Model(value: String) : BaseDataComponent() { + var value: String by obs(value) + } + + val model = observableListOf(Model("First"), Model("Second")) + val container = DataContainer(model, { m, _, _ -> Label(m.value) }, VPanel()) + root.add(container) + val element = document.getElementById("test") + assertEqualsHtml( + "<div style=\"display: flex; flex-direction: column;\"><div><span>First</span></div><div><span>Second</span></div></div>", + element?.innerHTML, + "Should render correct data container" + ) + model.add(Model("Third")) + assertEqualsHtml( + "<div style=\"display: flex; flex-direction: column;\"><div><span>First</span></div><div><span>Second</span></div><div><span>Third</span></div></div>", + element?.innerHTML, + "Should render correct data container after model change" + ) + model[1].value = "Changed" + assertEqualsHtml( + "<div style=\"display: flex; flex-direction: column;\"><div><span>First</span></div><div><span>Changed</span></div><div><span>Third</span></div></div>", + element?.innerHTML, + "Should render correct data container after model element change" + ) + } + } + +}
\ No newline at end of file |