digestive-functors for Snap users
Published on April 3, 2012 under the tag haskell
The past week there’s been a bit of work done on better integration between the digestive-functors library and the Snap framework.
Daniel Patterson has been so kind to implement file uploads for the Snap backend of digestive-functors – this was previously only implemented in the Happstack. The great thing is that this implementation still has all flexible options Snap allows for file uploads.
For now, digestive-functors enforced you to use the blaze-html library, simply because no other frontend library was available. Snap users will be delighted to hear we’ve written a Heist library now. Let’s take a look at an example. I’ll go over the forms rather quickly and focus on the Heist library. If this is the first time you read about digestive-functors, you might want to take a glance at this tutorial first.
First, we need some datatypes:
data Date = Date
dateDay :: Int
{ dateMonth :: Int
, dateYear :: Int
,deriving (Show)
}
data Sex = Female | Male
deriving (Eq, Show)
data User = User
userName :: Text
{ userPassword :: Text
, userSex :: Sex
, userBirthdate :: Date
,deriving (Show) }
And then some forms:
dateForm :: Monad m => Form Text m Date
= check "Not a valid date" validDate $ Date
dateForm <$> "day" .: stringRead "Not a number" (Just 16)
<*> "month" .: stringRead "Not a number" (Just 6)
<*> "year" .: stringRead "Not a number" (Just 1990)
where
Date day month _) =
validDate (>= 1 && day <= 31 &&
day >= 1 && month <= 12
month
userForm :: Monad m => Form Text m User
= User
userForm <$> "name" .: text (Just "Jasper")
<*> "password" .: text Nothing
<*> "sex" .: choice [(Female, "Female"), (Male, "Male")] Nothing
<*> "birthdate" .: dateForm
Because of the composable nature of digestive-functors, we will first write a template for the date form, and reuse that for the user form.
Let’s place the template for the date form in snaplets/heist/date-form.tpl
.
Almost all splices take a ref
attribute, which tells the library what the
field is for – by doing this, we automatically get the previous value if a form
submission fails, etc. We can also easily pass arbitrary attributes to the input
elements, like we do with size
here.
<dfInputText ref="day" size="2" />
/<dfInputText ref="month" size="2" />
/<dfInputText ref="year" size="4" />
There’s a bit more in snaplets/heist/user-form.tpl
.
dfForm
generates a<form>
tag, and takes care of themethod
andenctype
attributes for us – we just have to take care ofaction
.We use
dfChildErrorList
withref=""
to generate a list of all errors. It is also possible to generate the errors next to the relevant fields, should you wish to do so.There’s obviously more than
dfInputText
– we havedfInputPassword
here, anddfInputSelect
to generate a combobox.In order to reuse the
date-form
template, we first usedfSubView
. This changes the focus on the “form tree”, so ourdate-form
template can usemonth
instead ofbirthdate.month
. Theapply
splice is a standard Heist splice.
<dfForm action="/">
<dfChildErrorList ref="" />
<dfLabel ref="name">Name: </dfLabel>
<dfInputText ref="name" />
<br>
<dfLabel ref="password">Password: </dfLabel>
<dfInputPassword ref="password" />
<br>
<dfLabel ref="sex">Sex: </dfLabel>
<dfInputSelect ref="sex" />
<br>
Birthday:<dfSubView ref="birthdate">
<apply template="date-form" />
</dfSubView>
<br>
<dfInputSubmit value="Enter" />
</dfForm>
The digestive-functors-heist
library provides no Snaplet but integrates well
with the concept. We just need to call runForm
from the
digestive-functors-snap
library, and most is taken care of:
form :: Handler App App ()
= do
form <- runForm "form" userForm
(view, result) case result of
Just x -> -- do something with the user in x...
Nothing -> heistLocal (bindDigestiveSplices view) $ render "user-form"
I hope this quick glance at the library was useful. Any comments and criticism
are welcome, as always. The full source code for this example can be found
here
(snap-heist.hs
, and the templates in snaplets/heist/templates
).