Hello everyone!
In the last few months I've been experimenting with fp and I've been enjoing fp-ts.
I am writing a small application which is basically filled with lots of forms, so I am writing here to discuss
together about possible implementations of form managing in the fp world.
Together with fp-ts I'm using elm-ts, so I am using the elm architecture, but that's only an implementation detail for the UI,
so I'll describe that in the end of the prerequisites.
The form primitives should support these functionalities:
Plus features:
This part of the question is more towards embedding the types/patterns defined above with elm-ts.
Plus features:
Up to now, this is the approach I've been experimenting with:
Field<A, O, I> is a plain io-ts encoder/decoder, so it's just a re-export of the Type exposed by io-ts. This satisfy the points no. 1, 2 and 3.
The Form<T> is a type that extends StrMap<Field> where the key is the field name. The Form type is also an encoder/decoder from Record<string, unknown> to the encoded form type, this also satisfy the point no. 5.
Let's say we have a form with a name and a birthday date.
The name field is just a t.string from io-ts.
The birthday field is DateFromISOString from io-ts-types.
The view function takes as parameter the Form and the raw value record, and outputs the Html. To do so I've introduced StrMapRenderer:
class StrMapRenderer<T, A, B>{
constructor(
public readonly item: (key: string, item: T) => A,
public readonly group: (items: StrMap<A>) => B
)
}
StrMapRenderer<T, A, B> is able to reduce an StrMap<T> to B by applying a mapWithKey function first (the item fn), and then the group fn.
The item function can look at the field type, so it can use a different renderer (with the same return type) based on the field type.
The renderer is able to return functions, so I am able, for example, of wrapping an existing renderer to append additional field needed by the render step.
e.g. I need to wrap the fields with a given label, the renderer has the following signature StrMapRenderer<Field, Html<Msg>, Html<Msg>> I can produce a new renderer that wraps that and instead has this signature StrMapRenderer<Field, (labels: string) => Html<Msg>, (labels: StrMap<string>) => Html<Msg>> and internally call the previous renderer item and group function.
I would like to be able to pass some "env" to the decode functions of my fields. This is because if I have a field that is an ID that needs to be validated agains a set of records, I need to pass somehow a function like (getValidUserId: (userId: string) => Option<number>) => Field<number, string, string> and use that in the decode function of the returned field. Other env vars also needs to be also accessed by the view, to renderer the user name side by side the user ID.
Unfortunately the encoding proposed before is not great for this task, because having a function that returns the field instance means rebuilding the form instance each time one of the parameters change its parameters, and that may vary when changing the field type.
The UI prerequisites written above are just to explain my desidered use case, they can be _almost easily_ be implemented by keeping a values property with the raw values of the form as Record<string, unknown> and a Option<string> field which stores the currently focused field.
An array of dirty fields can also be kept to distinguish between currently editing values and already validated value. Once the user blurs a field, the field value is validated, and if valid the field name is removed from the dirty field list. The form from the UI exposes a decoder method that returns a value only if there are no dirty fields, and form wide validation passes.
I have also experimented a bit with a form DSL, but the problem presented before is still there, because when rendering through the DSL the render function needs to know which are the required parameters based on the handled field types.
How do you handle forms? Do you use any library? Which are your current concerns with your solution? Does your solution solve one of the previous problems? Let's discuss together and learn new things :)
Today I had the pleasure to chat a little bit with @gcanti and gave some great ideas and input about rethinking forms.
First of all, one great way of thinking about API is to imagine being a customer using a third party API. Which would be the API I would love to use?
If a service would take care of rendering and displaying the form together with errors, what would I care about? Obviously only about getting the form data.
Let's say that "V" is the raw field value, and "A" is the validated field output. A good guess would be to have something like: (value: V) => Observable<Option<A>>. The return value being an observable is great, because it allows the return value to be validated asynchronusly, and when the user enters the editing, a none will be triggered by the observable. This way the API is'nt tight on returning just the first valid value, but may also keep emitting any valid value through the entire form life, so the API consumers can listen for just the first, or every valid value emitted by the form.
Resource: "Using PureScript to create a domain-specific language for building forms with validation"
Most helpful comment
Resource: "Using PureScript to create a domain-specific language for building forms with validation"
https://medium.com/fuzzy-sharp/building-a-type-safe-embedded-dsl-for-form-components-with-validation-e7ffaaf537e4