January 3, 2014 - Tagged as: haskell, en.
EDIT: I just found this awesome “Heist in 60 seconds” post in Hesit’s author’s blog, strongly recommended.
I’m currently learning some web programming related libraries for Haskell and I’m very, very confused because of the need for using more than 20 libraries for even the simplest CRUD webapp. In the end I’ll be using Snap, Heist, Digestive-functors, Persistent, Esqueleto and glue libraries to combine all of this together. After wasting several hours trying to learn all of them at once, I decided to move gradually from simplest parts. In this short tutorial, I’ll explain how to create forms using digestive-functors, render them in HTML and run some validation procedures.
This post is written in Literate Haskell, except for the last part, which contains a Heist template for rendering our forms in HTML.
In my opinition, there are two problems for starters of Haskell web programming in Snap. First, Snap lacks some important web development functionalities and for that you need to use separate libraries. This includes form generation and rendering, database operations and probably many others. (on the other hand, we have very high quality libraries so this part may not be a problem, depending on your point of view)
Second, while using other libraries you realize that most of the time documentation is not very good and some important details for starters are missing, when that happens you end up diving into the source code and open source examples.
Anyway, I hope this post serves as an example for using Digestive-functors and Heist together for handling user inputs.
A note before starting: I don’t understand how compiled splices of Heist works, I tried using them but for some reason I couldn’t make it working. So in this post I’ll be using interpreted splices only.
Let’s start with some langauge extensions. You’ll see why this extension is needed below
{-# LANGUAGE ScopedTypeVariables #-}
This one is required for pattern matching against Text values
{-# LANGUAGE OverloadedStrings #-}
module Main where
Even though our program doesn’t do anything interesting, we still need to use about 10 libraries. I’m showing the package name when a non-standard(e.g. the ones distributed with Haskell Platform) module is imported.
from `blaze-builder’ package
import Blaze.ByteString.Builder (toByteString)
import Control.Applicative (Applicative (..), (<$>), (<*>))
import Control.Monad.IO.Class (MonadIO)
from `either’ package
import Control.Monad.Trans.Either (EitherT, runEitherT)
from `bytestring’ package, needed for putStrLn function on bytestrings
import qualified Data.ByteString.Char8 as BS (putStrLn)
import Data.Maybe (isJust)
import qualified Data.Text as T
from `heist’ package
import Heist
import qualified Heist.Interpreted as HI
from `digestive-functors’ package
import Text.Digestive
from `digestive-functors-heist’ package
import Text.Digestive.Heist (bindDigestiveSplices)
In this program, we’ll have one data type that represents a User in our app. I’m planning to extend this post later on to add CRUD database operations on same data type.
data User = User
uUsername :: T.Text
{ uEmail :: T.Text
, uKarma :: Int
,deriving (Show) }
userForm
is a digestive-functors form for User type, which is used for creating new User and modifying existing User.
In the return type:
.:
operator is used to assign a name to form fields. This names are later used in templates, POST request environments and probably in some other places.
userForm :: Monad m => Form T.Text m User
= User
userForm <$> "username" .: text Nothing
<*> "email" .: check "invalid email" validateEmail (text Nothing)
<*> pure 0
where
-- in our example, we don't need to use `m` monad for validation. if we
-- were to need that, we could use `checkM` instead of `check`, and
-- then use a validation function with type `T.Text -> m Bool` for same m.
validateEmail :: T.Text -> Bool
= isJust . T.find (== '@') validateEmail
For generating HTML using Heist, we need to maintain HeistState
type, which keeps track of information that is needed for rendering templates.
In the code below, `m’ is called “runtime monad” and represents the monad type that rendering functions operate on. We will see an example use later.
I’m using ScopedTypeVariables
extension to share type parameter m
with type declarations in where
part. This is only optional, since I could always use let
or just inline the heistConfig
definition.
initHeistState :: forall m. MonadIO m => IO (HeistState m)
= do
initHeistState <- runEitherT $ initHeist heistConfig
st case st of
Left errors -> error $ unlines errors
Right state -> return state
where
heistConfig :: HeistConfig m
-- In HeistConfig, we need to specify what load-time, compile-time and
-- run-time splices will be available. We also have options for
-- attibute splices(QUESTION: why we don't have time distinction in
-- attribute splices?) and locations of template files.
= HeistConfig
heistConfig
{-- default interpreted splices consists of
-- `apply`, `bind`, `ignore` and `markdown` splices
= defaultInterpretedSplices
hcInterpretedSplices -- this is same as default interpreted splices
= defaultLoadTimeSplices
, hcLoadTimeSplices -- I'm not using compiled splices because of the reason explained
-- in first paragraphs
= []
, hcCompiledSplices -- .. also here.
= []
, hcAttributeSplices -- list of template locations. A template location is an
-- IO action that either returns a list of error strings,
-- or a map from template locations to template files.
-- We're using `loadTemplates` from Heist package for loading
-- templates from a folder. Subfolders are also traversed.
= [loadTemplates "templates"]
, hcTemplateLocations }
This is our function to render form templates written in Heist template format using digestive-functors forms. Digestive-functors forms are not directly renderable, instead we need a View
object generated from Form
using getForm
or postForm
from Text.Digestive.View
(digestive-functors
package).
renderForm :: HeistState IO -> View T.Text -> IO ()
= do renderForm hs formView
Because of a problem, we can’t use digestiveSplices form
to get splices and then bind them manually using bindSplices
. I think this is because of a type mismatch caused by current versions of digestive-functors-heist and heist libraries. So we need to use bindDigestiveSplices
from Text.Digestive.Heist
(digestive-functors-heist
package).
<- HI.renderTemplate (bindDigestiveSplices formView hs) "user_form"
maybeBuilder case maybeBuilder of
Nothing ->
-- This happens when wrong template name is given to `renderTemplate`.
error "template is not rendered"
Just (builder, mimeType) -> do
-- here `builder` has type `Blaze.ByteString.Builder.Builder` from
-- `blaze-builder` package. It's used to efficiently build
-- ByteStrings.
BS.putStrLn (toByteString builder)print mimeType
I’m just printing stuff, since this app is mostly for learning purposes.
In main function, I do three things:
validateEmail
function above, and error message was specified in userForm
function.After rendering the HTML code, I’m just printing it. Also, form rendering function(postForm
) returns an optional User object depending on the validness of information from POST request. I’m also printing that User object.
main :: IO ()
= do
main <- initHeistState
hs
-- we need to dynamically bind splices related with form generation
-- while rendering `user_form` heist template. for that we need to use
-- `Heist.Interpreter` functions to modify interpereted splices of our
-- heist state.
--
-- To get form splices, we need to pass some POST or GET request as
-- ByteString to `Text.Digestive.View.getForm` or `postForm`. Then we
-- can use `Text.Digestive.Heist.digestiveSplices` to get required
-- splices to render form.
-- Here the type T.Text comes from first argument of userForm's return
-- type
<- postForm "userform" userForm (const $ return [])
(formView, maybeUser) :: IO (View T.Text, Maybe User)
print maybeUser
renderForm hs formView
-- Case 2, POST request with invalid email address
let env path = return $ case path of
"userform", "username"] -> [TextInput "testuser"]
["userform", "email"] -> [TextInput "invalidemail"]
[-> []
_ <- postForm "userform" userForm env
(formView', maybeUser') print maybeUser'
renderForm hs formView'
-- Case 3, POST request with valid email address and username
let env' path = return $ case path of
"userform", "username"] -> [TextInput "testuser"]
["userform", "email"] -> [TextInput "valid@email.com"]
[-> []
_ <- postForm "userform" userForm env'
(formView'', maybeUser'') print maybeUser''
renderForm hs formView''
Now our program is almost complete, only detail left is the Heist template file. We specified the template path in initHeistState
as templates
folder, and we’re rendering Heist template named user_form
in renderForm
. So the template file we need should be templates/user_form.tpl
.
Here’s the template file:
<dfForm>
<dfLabel ref="username">Username: </dfLabel>
<dfInputText ref="username" />
<dfErrorList ref="username" />
<dfLabel ref="email">Email: </dfLabel>
<dfInputText ref="email" />
<dfErrorList ref="email" />
<dfInputSubmit />
</dfForm>
One problem here is that there’s no way to know which tags to put in template file. I wrote this file mostly by looking to source code of bindDigestiveSplices
, trial-and-error, and some open source examples.
Output should be something like: (after creating the template file, see below)
Case 1:
Nothing
because POST request environment is not valid, so it’s not possible to create a User object.
<form method='POST' enctype='application/x-www-form-urlencoded'>
<label for='userform.username'>Username: </label>
<input type='text' id='userform.username' name='userform.username' value />
<label for='userform.email'>Email: </label>
<input type='text' id='userform.email' name='userform.email' value />
<ul><li>invalid email</li></ul>
<input type='submit' />
</form>
User form is generated without filling any values and no error messages.
Case 2:
Nothing
because email information in POST request environment is invalid.
<form method='POST' enctype='application/x-www-form-urlencoded'>
<label for='userform.username'>Username: </label>
<input type='text' id='userform.username' name='userform.username' value='testuser' />
<label for='userform.email'>Email: </label>
<input type='text' id='userform.email' name='userform.email' value='invalidemail' />
<ul><li>invalid email</li></ul>
<input type='submit' />
</form>
User form is generated with fields filled and an error message is rendered.
Case 3:
Just (User {uUsername = "testuser", uEmail = "valid@email.com", uKarma = 0})
Since form data is valid, a User object is created.
<form method='POST' enctype='application/x-www-form-urlencoded'>
<label for='userform.username'>Username: </label>
<input type='text' id='userform.username' name='userform.username' value='testuser' />
<label for='userform.email'>Email: </label>
<input type='text' id='userform.email' name='userform.email' value='valid@email.com' />
<input type='submit' />
</form>
.. and for is created with values filled, no error messages is rendered.
Note the form and input ids and names. The name passed to postForm
is used as prefix of generated HTML elements, and thus also used in POST request environments.
I hope this post helps starters with digestive-functors and heist.