Hi Robert, me again. I am facing a problem. I'm t...
# kvision
i
Hi Robert, me again. I am facing a problem. I'm trying to use forms with a custom value and don't know how I should do it. I created custom components (I'm not sure if I did them right as they don't work right now, but the issue I'm writing about is related to another issue)
filterPanel
filterAutocompleteItem
houseAutocomplete
And I want to use them like this:
filterPanel(submitFormCallback = {
println(it.getData())
}) {
filterAutocompleteItem("Address") {
houseAutocomplete()
}
}
My problem is that I can't set the value with Any type. To make it clearer what I mean and why I need this, here is my case: In
houseAutocomplete()
, I receive data from the backend and form a list of options, when the user clicks, I want to set my
data class
as the field value for the
form
. But I'm limited by the fact that the field type I'm using on the form only accepts a string as a value (which is logical and correct). Also, I can't use Serialization for the value, since I don't want to show the user the complete serialized object. What I want is the ability to set a custom value for a form field. It does not need to be serialized or deserialized. Since it does not need to be explicitly shown to the user. For example,
houseAutocomplete()
could set
data class AutocompleteOption(val label: String, val value: Any)
as the
customValue
for
textInput
and just a
label: String
as the
value
And for the form one could use the
getCustomData()
method to get the custom values What do you think about it ? I understood correctly that this cannot be achieved now ? Or is there another way ?
r
I'm afraid I don't understand. Let's start from the beginning. Is you
FilterPanel
a
FormPanel<T>
? What is
T
in your case? How do you use form controls (e.g.
TextInput
)? How do you
bind()
your controls to the form panel?
i
After your post, I rewrote the code a bit. val state = FormState() is now responsible for storing my values
Copy code
data class FormState(val fields: ObservableValue<MutableMap<String, Any?>> = ObservableValue(mutableMapOf())) {
    fun clear() { fields.setState(fields.value.also { it.clear() }) }
    fun get(key: String) = fields.value[key]

    fun<T> getSub(key: String): ObservableState<T> = fields.sub { it[key] as T }
    fun set(pair: Pair<String, Any?>) { fields.setState(fields.value.also { it[pair.first] = pair.second  })}
}
Here's my FilterPanel
Copy code
fun Container.filterPanel(submitFormCallback: ((FormState) -> Unit), contentBuilder: FormPanel<Map<String, Any?>>.(FormState) -> Unit) {
    val state = FormState()

    div (className = "filter__list"){
        form (className = "filter__blocks -collapsed") {
            contentBuilder(state)
            div(className = "filter__block important") {
                button("", className = "btn-apply").onClick { submitFormCallback(state) }
                div {
                    button("", className = "btn-reset").onClick { state.clear(); submitFormCallback(state) }
                    button("", className = "btn-all")
                }
            }
        }
    }
}
Copy code
fun FormPanel<Map<String, Any?>>.filterField(label: String, state: FormState, contentBuilder: Container.(FormState) -> Unit) {
    div (className = "filter__block important") {
        div(className = "field") {
            p(label)
            contentBuilder(state)
        }
    }
}
Copy code
fun FormPanel<Map<String, Any?>>.houseAutocomplete(state: FormState, key: String = "house_id") {
    textInput().bind(ObservableValue("Some Start Value")) { stateValue ->
        startValue = stateValue
        onInput {
            state.set(key to AutocompleteOption(value, 5))
        }
    }
}
and i use it like this
Copy code
filterPanel(submitFormCallback = {
    println(it.fields.value)
}) { formState ->
    filterField("Address") { houseAutocomplete(formState) }
}
Everything seems to work (at least setting the values), but when I hit the clear button, the state is reset but the component doesn't re-render, which again makes sense because it's bound to ObservableValue("Some Start Value"). And now the problem is that I do not know what I need to bind the component to so that its value is cleared. Maybe I've overcomplicated it. Initially, I wanted to be able to save a custom value for the form field.
The problem with binding in this case is the duplication of the event. I didn't notice this problem before, what am I doing wrong? When typing each new character, the console.log message is duplicated
Copy code
data class FormValue(
    val value: Any? = null,
    val customValue: Any? = null
)
data class FormState(val fields: ObservableValue<MutableMap<String, FormValue>> = ObservableValue(mutableMapOf())) {
    fun clear() { fields.setState(fields.value.also { it.clear() }) }
    fun get(key: String) = fields.value[key]

    fun <T>getSub(key: String): ObservableState<T> = fields.sub { it[key]?.value as T }
    fun set(pair: Pair<String, FormValue>) { fields.setState(fields.value.also { it[pair.first] = pair.second  })}
}
r
It's probably a bit overcomplicated ;-), but I've played with your code and come up with this:
Copy code
data class CustomValue(val data: Any?)

data class FormValue(
    val value: String? = null,
    val customValue: Any? = null
)

data class FormState(val fields: ObservableValue<Map<String, FormValue>> = ObservableValue(emptyMap())) {
    fun clear() {
        fields.setState(emptyMap())
    }

    fun get(key: String) = fields.value[key]
    fun getSub(key: String): ObservableState<FormValue?> = fields.sub { it[key] }
    fun set(pair: Pair<String, FormValue>) {
        fields.value += pair
    }
}

fun Container.filterPanel(
    submitFormCallback: ((FormState) -> Unit),
    contentBuilder: FormPanel<Map<String, Any?>>.(FormState) -> Unit
) {
    val state = FormState().apply {
        set("house_id" to FormValue("My house", CustomValue("My house")))
    }

    div(className = "filter__list") {
        form(className = "filter__blocks -collapsed") {
            contentBuilder(state)
            div(className = "filter__block important") {
                button("apply", className = "btn-apply").onClick {
                    submitFormCallback(state)
                }
                div {
                    button("reset", className = "btn-reset").onClick { state.clear(); submitFormCallback(state) }
                    button("", className = "btn-all").onClick {
                    }
                }
            }
        }
    }
}

fun FormPanel<Map<String, Any?>>.filterField(
    label: String,
    state: FormState,
    contentBuilder: Container.(FormState) -> Unit
) {
    div(className = "filter__block important") {
        div(className = "field") {
            p(label)
            contentBuilder(state)
        }
    }
}

fun FormPanel<Map<String, Any?>>.houseAutocomplete(state: FormState, key: String = "house_id") {
    val sub = state.getSub(key)
    textInput() {
        bind(sub, { it?.value }) { value ->
            this.value = value
        }
        onInput {
            state.set(key to FormValue(value, sub.getState()?.customValue))
        }
    }
}
I've simplified FormState with no mutable map (a mutable map inside and observable value is too much mutability for me 🙂)
text input is bound to the substate formvalue and initialized with the first value on input event it sets the first value to the text data and keeps original customvalue
i
Thanks a lot, you're awesome ! From your code, I understood that the duplication in the console was due to the use of
onInput {
inside the
bind
function lambda
Copy code
textInput().bind(subscribe) {
    onInput {//it was a bad idea
        
    }
}
I also took the rest of your recommendations and will do as you suggested.