rjaros / kvision

Object oriented web framework for Kotlin/JS
https://kvision.io
MIT License
1.2k stars 67 forks source link

How to pass a serializer parameter for Tabulator/formatterComponentFunction #476

Closed joerg-rade closed 1 year ago

joerg-rade commented 1 year ago

I'm banging my head against an issue that is very similar to https://github.com/rjaros/kvision/issues/467.

The class I want to use with Tabulator (Exposer) is annotated with @Serializable. I don't see any log output in this case:

   private fun buildVega(it: PropertyDetails): ColumnDefinition<Exposer> {
        return ColumnDefinition(
            title = it.name,
            field = it.id,
            width = (it.typicalLength * 8).toString(),
            headerFilter = Editor.INPUT,
            formatterComponentFunction = { _, _, data ->
                console.log("[CF_buildVega]")
                console.log(data)
                this.buildDiagramPanel(data)
            })
    }

How can I pass on the serializer? vega_in_tabulator_debug

rjaros commented 1 year ago

I'm not sure I understand your question. You pass a serializer parameter to the tabulator() DSL builder function (in most cases it should be automatically infered because serializer() is its default value)

joerg-rade commented 1 year ago

I build my ColúmnDefinitions as follows:

    fun buildColumns(displayCollection: CollectionDM): List<ColumnDefinition<Exposer>> {
        val columns = mutableListOf<ColumnDefinition<Exposer>>()
        columns.add(columnForObjectIcon(displayCollection))
        columns.addAll(columnsForProperties(displayCollection))
        columns.add(columnForObjectMenu())
        return columns
    }

Exposer allows to access values by name:

@Serializable
class Exposer(val delegate: TObject) {

    var iconName = ""  //required by ColumnFactory

    fun dynamise(): dynamic {
        val thys = this.asDynamic()
        for (m in delegate.members) {
            val member = m.value
            if (member.memberType == MemberType.PROPERTY.type) {
                val realValue = member.value
                if (realValue != null) {
                    thys[member.id] = realValue.content
                }
            }
        }
        iconName = IconManager.find(delegate.title)
        if (iconName == IconManager.DEFAULT_ICON) {
            iconName = IconManager.find(delegate.domainType)
        }
        return thys
    }

    // eg. for dataNucleusId
    fun get(propertyName: String): Any? {
        return this.delegate.getProperty(propertyName)?.value
    }

    fun setIcon(icon: Icon) {
        this.asDynamic()["icon"] = icon.image.src
    }

}
joerg-rade commented 1 year ago

vega_illegal_argument_exception

rjaros commented 1 year ago

You are using the legacy backend so I assume you are not using the latest KVision (6.x). What version is this? And how do you create your Tabulator component?

joerg-rade commented 1 year ago

Yes - I was using legacy and KVision 5.x Trying to upgrade to ir/6.x was not successful so far - tables that used to work are broken now as well. (I'll probably stick with 5.x)

My design is centered around "dynamic" which is probably better explained in the following diagram:

exposer

Currently I think there is no way to define a serializer. Am I wrong?

rjaros commented 1 year ago

It should always be possible to define a custom serializer. What's more, the serialization part is trivial, because dynamize() is almost what you need (you just need one more JSON.stringify() call). But the deserialization part is probably more of a challenge. Do you have any means to create an Exposer instance out of the dynamic json data?

joerg-rade commented 1 year ago

No - Exposer is clientside only. Changes are applied to member.values and are eventually communicated via rest calls to the server.

rjaros commented 1 year ago

With KVision 5 and the legacy backend, the tabulator component can be used with three model types:

  1. With a serializable kotlin class and a serializer capable of converting this class to a JS object and back.
  2. With a kotlin data class, which is in general "compatible" with a JS object (has the same properties)
  3. With a fully dynamic JS objects (no conversion needed at all)

Unfortunately your case is neither of those, that's why it's not working correctly. If creating the serializer is not possible, I think you should try to use third option - Tabulator<dynamic> instead of Tabulator<Exposer>. Transform your data with map

list.map {
    it.dynamize()
} 

and use the transformed list as tabulator input. You would have to refactor buildColumns to use dynamic data instead of Kotlin class, but it should be possible.

joerg-rade commented 1 year ago

Is the recommended method the same with ir/6? (I'm mostly done with the migration)

rjaros commented 1 year ago

Yes, in KV 6 only option 1. and 3. are possible, so you also need to go with dynamic.

joerg-rade commented 1 year ago

My table is still not visible. I've introduced Exhibit for serialization purposes (see https://github.com/apache/causeway/blob/kvision-6-upgrade/incubator/clients/kroviz/src/main/kotlin/org/apache/causeway/client/kroviz/core/model/Exposer.kt)

When I debug the created tabulator I see: tabulator_data and: tabulator_serializer

What am I missing?

rjaros commented 1 year ago

Can I somehow run the project and see the problem on my local machine?

joerg-rade commented 1 year ago
docker pull apache/causeway-app-demo-jpa:latest
docker run --publish 8080:8080 -ePROTOTYPING=true apache/causeway-app-demo-jpa:latest
git clone https://github.com/apache/causeway
cd causeway
git checkout kvision-6-upgrade
cd ~/incubator/clients/kroviz
gradle run

http://localhost:3000/

BurgerMenu -> Connect -> Url:localhost:8080/ -> OK Common Data Types -> Strings

Console should show something like:

[RT_before] tabulator(), model ->
kroviz.js:23295 ObservableListWrapper {mutableList_1: ArrayList, onUpdate_1: ArrayList}
kroviz.js:23299 Tabulator {propertyValues_1: {…}, propertyStyles_1: {…}, snStyleCache_1: null, width$delegate_1: RefreshDelegate_0, minWidth$delegate_1: RefreshDelegate_0, …}

& Thanks for looking into this!

rjaros commented 1 year ago

You are not adding tabulator to your components tree. That's why it doesn't show up at all. You are creating the component with createTabulator but you forgot to use add(...).

diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/causeway/client/kroviz/ui/core/RoTable.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/causeway/client/kroviz/ui/core/RoTable.kt
index 52d9c5abf9..37f98a177c 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/causeway/client/kroviz/ui/core/RoTable.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/causeway/client/kroviz/ui/core/RoTable.kt
@@ -76,6 +76,7 @@ class RoTable(displayCollection: CollectionDM) : SimplePanel() {
             }
         }
         tabulator.redraw(true)
+        add(tabulator)
         console.log(tabulator)
     }

After fixing this I see the table on the page, but I'm not sure it looks correct. If you need more assistance please let me know.

joerg-rade commented 1 year ago

What I have so far only partly works:

class Exposer(val delegate: TObject) {
    val iconName: String = IconManager.findFor(delegate) //required by ColumnFactory
    val exhibit: Exhibit

    init {
        val delegateUrl = getDelegateUrl()
        exhibit = Exhibit(delegateUrl)
        val that = exhibit.asDynamic()
        that["icon"] = null
        for (m in delegate.members) {
            val member = m.value
            if (member.memberType == MemberType.PROPERTY.type) {
                val realValue = member.value
                if (realValue != null) {
                    that[member.id] = realValue.content
                }
            }
        }
    }
}

@Serializable
class Exhibit(val url: String) {}

url is set via constructor and property names are amended via that[member.id] - but realValue.content doesn't break on through to the other side ;-). Exhibit_w_values(kotlin) Exhibit_withOut_values

Any idea what else I could try? obj {} ?

rjaros commented 1 year ago

One step further ... When using Tabulator with a dynamic model you need to initialize it a bit differently. Instead of passing your model as the first parameter of the constructor you need to pass it as a data property of TabulatorOptions. I realize it's a bit unintuitive - I've tried to hide this behind the factory functions parameters and even added a sample in the guide. But you are using the class constructor directly so nothing warns you about the problem. With this fix I can see some data in the table (dynamic data also means no need for the serializer and probably the Exibit class can be completely removed):

diff --git a/incubator/clients/kroviz/src/main/kotlin/org/apache/causeway/client/kroviz/ui/core/RoTable.kt b/incubator/clients/kroviz/src/main/kotlin/org/apache/causeway/client/kroviz/ui/core/RoTable.kt
index 1658182640..762625d44a 100644
--- a/incubator/clients/kroviz/src/main/kotlin/org/apache/causeway/client/kroviz/ui/core/RoTable.kt
+++ b/incubator/clients/kroviz/src/main/kotlin/org/apache/causeway/client/kroviz/ui/core/RoTable.kt
@@ -80,8 +80,7 @@ class RoTable(displayCollection: CollectionDM) : SimplePanel() {
         val className: String? = null
         val init: (Tabulator<dynamic>.() -> Unit)? = null
         val tableTypes = setOf(TableType.STRIPED, TableType.HOVER)
-        val serializer = Exhibit::class.serializer()
-        val tabulator = Tabulator(data, dataUpdateOnEdit, options, tableTypes, serializer = serializer)
+        val tabulator = Tabulator(null, dataUpdateOnEdit, options.copy(data = data.toTypedArray()), tableTypes)
         if (className != null)
             tabulator.addCssClass(className)
         init?.invoke(tabulator)
joerg-rade commented 1 year ago

Thanks - works now. (When I do not see the forest for the trees, I tend to take some down ... This time it looks like I threw out the baby with the water ;-)