aboutsummaryrefslogtreecommitdiff
path: root/kvision-modules/kvision-datacontainer
diff options
context:
space:
mode:
Diffstat (limited to 'kvision-modules/kvision-datacontainer')
-rw-r--r--kvision-modules/kvision-datacontainer/build.gradle15
-rw-r--r--kvision-modules/kvision-datacontainer/package.json.d/project.info3
-rw-r--r--kvision-modules/kvision-datacontainer/src/main/kotlin/pl/treksoft/kvision/data/DataComponent.kt54
-rw-r--r--kvision-modules/kvision-datacontainer/src/main/kotlin/pl/treksoft/kvision/data/DataContainer.kt216
-rw-r--r--kvision-modules/kvision-datacontainer/src/main/kotlin/pl/treksoft/kvision/data/DataUpdatable.kt29
-rw-r--r--kvision-modules/kvision-datacontainer/src/test/kotlin/test/pl/treksoft/kvision/TestUtil.kt99
-rw-r--r--kvision-modules/kvision-datacontainer/src/test/kotlin/test/pl/treksoft/kvision/data/DataContainerSpec.kt69
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