Dynamic List Formlets in Haskell
The Haskell Formlets library (described here) provides an elegant abstraction that takes the tedium out of HTML form generation, validation, and processing (not to mention make it less error prone). This abstraction allows you to develop HTML form UIs while still thinking in and working with haskell's type system and data structures. It also fits very naturally with Happstack's ideas about using in-memory state to leverage the pure type system.
- Default Values
- Error Handling
One critical aspect of form handling is the ability to specify default values for form fields. I encountered this issue very soon after I started using Formlets. If you want to edit the state of stored objects through an HTML form, you will probably want to be able to create a form where the object's current values for all the fields are already filled in. Then the user just changes the appropriate fields and doesn't have to re-enter all the other data. The Form type by itself is inadequate for this purpose. We added a new type alias to server this purpose:
> type Formlet xml m a = Maybe a -> Form xml m a
In my experience, working with Formlet instead of Form will make things easier in the long run. There are certainly simple situations where Form is sufficient, but Formlet is an important abstraction to have around.
The second important issue with list formlets in particular is error handling. When a single field has an error, the HTML whole form usually needs to be regenerated with an appropriate error indication. This is especially important in the context of list formlets.
Before continuing, we need to understand the type signature of a list formlet. Ultimately a list formlet will have the type
Form xml m [a]. But we don't want to construct these formlets manually. We would like to be able to specify the formlet for a single item and have a function that automatically constructs a formlet representing the list of items. In the old version of formlets, this function was called massInput. We might define massInput as follows:
> massInput :: Form xml m a -> Form xml m [a]
But this isn't quite right because, as described above, we need the ability to specify default values in a list form. Here's where our Formlet type synonym becomes nice:
> massInput :: Formlet xml m a -> Formlet xml m [a]
There are two common approaches to representing a list of items: arrays and linked lists. The same is true of lists of fields in HTML. We could do a linked list implementation by adding a hidden element to each list item and have the value of the hidden element somehow tell us how to determine the name of the field(s) for the next item in the list. A hidden item with an empty value would indicate the end of the list. This is the approach used by the old massInput implementation. Its hidden items contained the prefix to the names of all the fields for the next item.
The other possibility would be to just use a predictable numbering scheme for the field names of list items. Then, instead of traversing next pointers, we would just increment through the form values until we find one that doesn't appear in submitted data.
It wasn't until after I did a significant amount of work with the linked list approach that I realized it has a fatal flaw. When a list item doesn't validate properly, massInput isn't able to recover the value of the hidden "next pointer" for that item. This means that if a user enters a list of 10 items, but makes a mistake in the first item, then the resulting error form will only have one item in the list. An error in any item will require the user to re-enter all subsequent items in the list. If you are using lists of complex items, this can be a lot of work and frustration for the user.
New and Improved
The new version of massInput fixes these problems. It uses the array approach to avoid the problem of vanishing elements when a form has errors, and it allows you to specify a list of default values for the list. This massInput function is in the Text.Formlets package, and provides the core dynamic list functionality.
The last code has not been uploaded to Hackage yet, but it is available from the Github repository. If you have any questions, comments, or improvements please let us know.