Handling Multiple POST Forms in Yesod

By James Parker - February 14, 2017

Yesod, a web framework for Haskell, provides convenient functionality that makes it simple to render and parse web forms. Unfortunately, it is not as elegant when it comes to implementing a POST handler that can process multiple web forms. This blog post presents one approach that simplifies the processing of multiple web forms in a single handler.

The first step to handling multiple POST forms is to use the identifyForm function. This function takes a string that uniquely identifies a given form and embeds a hidden field in the form (which is given as the second argument). When parsing a request’s POST data, if the hidden field’s identifier does not match the form’s unique identifier, parsing will return a FormMissing. This allows the request handler to parse other forms until one parses successfully. Here is an example of how to use identifyForm:

-- Data type for formA. 
data FormDataA = FormDataA Text

-- Data type for formB. 
data FormDataB = FormDataB Int

-- Define formA.
formA :: Form FormDataA
formA = identifyForm "form-a" $ renderBootstrap3 BootstrapBasicForm $ FormDataA
    <$> areq textField "Some text" Nothing

-- Define formB.
formB :: Form FormDataB
formB = identifyForm "form-b" $ renderBootstrap3 BootstrapBasicForm $ FormDataB
    <$> areq intField "A number" Nothing

-- Handle POST requests.
postHandlerR :: Handler Html
postHandlerR = defaultLayout $ do
    ((res, widget), enctype) <- handlerToWidget $ runFormPost formA
    case res of
        FormMissing -> do
            ((res, widget), enctype) <- handlerToWidget $ runFormPost formB
            case res of
                FormMissing -> ...
                FormFailure _ -> ...
                FormSuccess _ -> ...
        FormFailure _ -> ...
        FormSuccess _ -> ...

While this example works, the nested structure found in the POST handler is unwieldy (especially as you keep adding more forms). We can eliminate this nested structure by introducing the following new data type and combinator.

data FormAndHandler = forall a . FormAndHandler (Form a) (FormResult a -> Widget -> Enctype -> Widget)

runMultipleFormsPost :: [FormAndHandler] -> Widget
runMultipleFormsPost [] = return ()
runMultipleFormsPost ((FormAndHandler form handler):t) = do
    ((res, widget), enctype) <- handlerToWidget $ runFormPost form
    case res of
        FormMissing ->
            runMultipleFormsPost t
        _ ->
            handler res widget enctype

The FormAndHandler data type is basically a pair that contains a form and a function that processing the form when submitted. The function runMultipleFormsPost takes a list of FormAndHandlers and attempts to parse each form until the first succeeds. Upon success, the corresponding handler is called with the parsed form result.

With this new functionality, we can rewrite the POST handler as follows:

postHandlerR :: Handler Html
postHandlerR = defaultLayout $ do
    runMultipleFormsPost [
        FormAndHandler formA formHandlerA
      , FormAndHandler formB formHandlerB
      ]

    where
        formHandlerA FormMissing = error "unreachable"
        formHandlerA (FormFailure _) = ...
        formHandlerA (FormSuccess _) = ...

        formHandlerB FormMissing = error "unreachable"
        formHandlerB (FormFailure _) = ...
        formHandlerB (FormSuccess _) = ...

By introducing FormAndHandler and runMultipleFormsPost, we have eliminated the nested structure of the previous example. This results in code that is more readable (and arguably more elegant) when a single handler expects multiple POST forms. You can find the full code for this post on Github.