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
-- 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
FormAndHandler data type is basically a pair that contains a form and a function that processing the form when submitted.
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 _) = ...
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.