Fp-ts: Discussion: A pure-functional way to handle model-based forms?

Created on 15 Oct 2018  路  2Comments  路  Source: gcanti/fp-ts

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.

Prerequisites

The form primitives should support these functionalities:

  • [ ] 1. there should be reusable field types definition _(e.g. string, date, select, checkbox, etc...)_
  • [ ] 2. each field type should support validation of its value _(e.g. the value is not numeric, the string is not a valid date given a format, the string should be at least N characters, etc...)_
  • [ ] 3. the form primitive should be able to encode/decode to/from a "raw" data type to a validated one _(e.g. the raw value is a Record and the validated value is {id: number, name: string})_
  • [ ] 4. a form cannot have 2 fields with the same name/identifier.
  • [ ] 5. the form should support form-wide validation based on multiple field values _(e.g. a form having a customer ID input and an address ID input should validate that the address ID is )_
  • [ ] 6. each field type should be rendered by a different renderer
  • [ ] 7. form view should be built up automatically based on form definition

Plus features:

  • [ ] 1. it should be possible to define new fields easily from a stored value/plain JSON object _(so a DSL is preferred)_

UI handling prerequisites

This part of the question is more towards embedding the types/patterns defined above with elm-ts.

  • [ ] the raw form value should be always stored
  • [ ] it should be possible to distinguish between dirty and already validated fields, and trigger validation either live while updating the raw value, or on field blur.
  • [ ] it should be possibile to store and pass view-related values to the user interface _(e.g. input label, is the field editable?, is the field visible? etc...)_
  • [ ] current focused value should be stored

Plus features:

  • [ ] the view-related values should be curried as the last parameter, so they can easily be customized for each form and be passed without any performance issues _(recreating the form instance should be cheap)_

Possible solution

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.

The problems

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.

UI handling

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.

Other possibilities

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.

Ending with a question:

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 :)

discussion out of scope

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

All 2 comments

Rethinking forms

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"

https://medium.com/fuzzy-sharp/building-a-type-safe-embedded-dsl-for-form-components-with-validation-e7ffaaf537e4

Was this page helpful?
0 / 5 - 0 ratings

Related issues

bioball picture bioball  路  4Comments

FruitieX picture FruitieX  路  3Comments

steida picture steida  路  4Comments

mustafaekim picture mustafaekim  路  4Comments

bobaaaaa picture bobaaaaa  路  4Comments