WebForms is an experiment in using WebViews as a form generator. Given a textual specification of a form, a web application for that form is automatically generated. The current WebForms system is a basic example, written in only a couple of days, but it can already generate an interesting range of forms. The size of the generator is about 600 lines of Haskell code, plus an additional 180 lines of JavaScript and css code. It should run on all major browsers and platforms as well as on mobile devices.

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.

A live form example

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 textAnswerElt, radioAnswerElt, or 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 isNumber = all isDigit). The validation is currently server-side only, but could be extended to the client by either providing a JavaScript validation function or compiling the Haskell validator to JavaScript.

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.