gi-gtk-declarative
gi-gtk-declarative copied to clipboard
[Question] Handling forms
Hi! First of all, I want to thank you for this awesome library. I never thought that writing UI in Haskell could be this easy.
I'm having a bit of trouble when it comes to the design and implementation of forms, and I'm unable to find a full example of this use case, so I'm not sure about how to implement it.
Currently, I have a view with a few Entries
, each for a specific input - let's say name
, surname
, email
, and a submit Button
.
-
The first solution that comes to mind is defining a field in my
State
that holds the current value of eachEntry
in some kind ofMap
. I can get the currentText
for eachEntry
using theonM #changed
function andentryGetText
, then I can fire anEvent
that updates theState
with the new value. This approach has a few advantages like on the fly validation and works fine for a small project, but let's say that my application has 20/30 forms. Would this scale? -
What I would like to do (don't know if this is the right way™) is to use a function that collects the text inside all my
Entries
and builds some kind ofMap
(id -> value
like in HTML forms) and fires anEvent
, something likeUserRegistrationEvent Fields
, whereFields
is a map of ids to values. This second approach would reduce the amount of code inside theState
record but sacrifices the on the fly validation. Also, sinceFields
is a kind ofMap
I'm a little worried about safety: I need to make sure that the values that I look up inside the map are valid - for example, if I look upphone-number
in my example I could get a runtime error.
Please, let me know what you think. I'll update the issue with some code snippets from my use case as soon as possible.
Also: I looked into the React-Redux ecosystem since gi-gtk-declarative uses a similar design pattern for UIs and I found redux-form. It would be interesting to have a similar library for this amazing project. Disclaimer: I primarily use Angular for my frontend projects so I'm not very confident in porting that library to Haskell
So far I have taken the approach of having an event (e.g. SetEmailField
) for each field in each form. This is simple and clear but involves a bunch of boilerplate code. For form state I have used data types rather than a generic Map
- for the type safety reasons you mention.
I would be interested in some kind of abstraction to reduce form layout / validation / event-handling boilerplate. I'd be tempted to try and copy something from the Elm universe, rather than Redux, since it is probably easier to translate from Elm rather than Javascript (but Haskell is more expressive than Elm so the gi-gtk-declarative
version might be nicer). In this case https://package.elm-lang.org/packages/hecrj/composable-form/8.0.1/ looks like it might work in gi-gtk-declarative
.
Indeed, there's a lot of boilerplate that can be reduced, but, as I said, I'm not an experienced Haskell dev. Defining an event for each field is a lot of work, especially in my use case, since my forms have in total over 30 fields.
In order to solve this I defined a forms :: Forms
field inside my State
, and every form exists inside this field. Then, I defined a UpdateForms Forms
event which replaces the current forms with the ones that comes in the event.
Now, the tricky part is updating each field for every form. I defined a FormField
type that works something like the following:
data FormField src a v = FormField {
_label :: Text, -- Used in prompts for this field (ex: "User name")
_value :: a, -- The kind of value of this field. Most of the times is Text
_setter :: src -> a -> src -- How to update this form field given a src (ex. Forms) and a new value
} ...
Now, when I create a FormField
I write a setter
function, that given a Forms
structure will update the field and return a new Forms
structure. Also, I use the v
type parameter for automatic safe parsing from a type a
to v
.
For example, a field userAge :: FormField Forms Text Int
is a field that will know how to convert its value to an Int
, how to update its value given that this field will live inside a Forms
structure and will also provide a label
There's a lot of boilerplate but this was the best I could do. You can see how this works in this toy project