Below is a sample form app that you can try out. The blue circles to the right indicate progress and turn red when entered text is not valid. The next-page button (labeled 'Volgende') is enabled once the page has been completely filled out. The entire form is specified in 115 lines of Haskell (source) and about 100 lines of css code (source) for describing table borders and colors. If your browser does not handle the iframe well, you can open a non-embedded version.
For simplicity, the form has been implemented as a single instance, which means that all users will view and edit the same form. For a more serious application, a multi-user form can be easily implemented using the built-in user-management functionality.
Specifying a form
The source code is straightforward Haskell code that constructs the form using the following functions (also called combinators):
form :: [FormPage] -> WebForm page :: [FormElt] -> FormPage tableElt :: String -> [[FormElt]] -> FormElt htmlElt :: String -> FormElt textAnswerElt :: QuestionTag -> FormElt radioAnswerElt :: QuestionTag -> [String] -> FormElt buttonAnswerElt :: QuestionTag -> [String] -> FormElt radioTextAnswerElt :: QuestionTag -> QuestionTag -> [String] -> FormElt styleElt :: String -> FormElt -> FormElt
The root of the form is a definition
mainForm = form [page_1, page_2 ... page_n] that puts the pages of the form together. Each
page takes a list of
FormElts, which are either an html fragment, an answer widget, or a nested table whose cells are again
FormElts. Html fragments are constructed with
htmlElt, and answer widgets are constructed with
buttonAnswerElt. Each of the answer elements has a string tag that determines its column in a csv file containing the results. The hybrid combinator
RadioTextAnswerElt is a radio widget that has a textual alternative at the bottom. It has two tags: one for the radio answer and one for the text (which may be empty). In order to style the form, css styles can be provided with the
styleElt combinator, or put in a separate css file, since the tables have a tag that is added to their html class.
For textual answers, there is an special combinator that supports validation of the answer:
textAnswerValidateElt :: QuestionTag -> (String -> Bool) -> FormElt
This combinator takes an extra function argument of type
(String -> Bool) to validate its answer and color the progress marker either blue or red. In the example above, the answer for the age field ('leeftijd' in Dutch) is specified with:
textAnswerValidateElt 'leeftijd' isNumber where
isNumber is the validator function (which is defined as
Because the form description is actually a Haskell program, common patterns can be expressed using functions. For example, we can create a question that has numbered buttons as its possible answers with the following function:
mkScaleQuestion :: Int -> String -> String -> TableRow mkScaleQuestion n tag question = [ htmlElt question, buttonAnswerElt tag $ map show [1..n] ]
Now we can use
mkScaleQuestion 10 'moeilijk' 'Ik vond het moeilijk om te kiezen' to get the following line in the form:
Another example of this abstraction are the vignettes on page 5 and 6 of the example form. The structure as well as most text on these pages is constant. Instead of building each page by hand, we can define a function
mkVignettePage (see source), which takes a record that contains only those part that are different and constructs a form page for it. This not only makes it much easier to add or modify vignette pages, but also makes it possible to restyle all of them in a single place. Moreover, it guarantees that all vignettes have exactly the same format.