Source

layout-bootstrap / Layout / Bootstrap / Forms.hs

Full commit
{-# LANGUAGE OverloadedStrings #-}

-----------------------------------------------------------------------------
-- |
-- Module: Layout.Bootstrap.Forms
-- 
-- Generic form builder and processor.
-- Not-so bootstrap, but uses it's widgets.
--
-- Example form:
--
-- > checkForm = (emptyForm []) { required = [ textField "Service" "test:pass" "Service ID. For example: «test:pass»."
-- >                                         , textField "Account" "9999995000" "Account ID. For example: «5077322496»." ]
-- >                            , optional = [ textField "Amount" "0.12" "Amount to check." ]
--
-----------------------------------------------------------------------------

module Layout.Bootstrap.Forms where

import Text.Blaze (ToHtml)
import Text.Blaze.Html5 (toHtml, toValue, (!))
import qualified Text.Blaze.Html5 as H
import qualified Text.Blaze.Html5.Attributes as A
import qualified Data.Text as T
import Control.Monad (forM, forM_)
import qualified Data.Map as HM

import Happstack.Server (lookTexts', ServerPartT)

import Layout.Bootstrap.Widgets (
  fieldset, form,
  simpleInput, Input(..),
  formActions, button)

-- * Form composition

type FormData = HM.Map T.Text [T.Text]

-- | Form container
data Form = Form { required :: [Input]
                 , optional :: [Input]
                 , extra    :: [Input]
                 , formValues :: FormData
                 , formErrors :: FormData
                 } deriving (Show)

instance ToHtml Form where
    toHtml form = do makeFields "Required" $! required form
                     makeFields "Optional" $! optional form
                     makeFields "Extra" $! extra form

                     case extra form of
                          [] -> ""
                          fs -> extraFields fs

                     formActions $! do
                        button "submit" ["btn-primary"] "Commit Request"
                        button "reset" [] "Reset Form"

                  where
                      value key = case HM.findWithDefault [] (T.toLower key) (formValues form) of
                                    [] -> ""
                                    [""] -> ""
                                    v:vs -> v

                      err key = HM.findWithDefault [] (T.toLower key) (formErrors form)

                      makeFields title fields = case fields of
                                                    [] -> ""
                                                    fs -> fieldset title $ mapM_ toHtml (withErrors . withValues $ fields)

                      withValues = map (\field -> case value (title field) of
                                                      "" -> field
                                                      fv -> field { value = fv })

                      withErrors = map (\field -> case err (title field) of
                                                      [] -> field
                                                      fe -> field { errors = True })

                      extraFields fs = forM_ fs (\f -> H.input
                                                       ! A.type_ "hidden"
                                                       ! A.name "extra"
                                                       ! (A.value . toValue . title $ f))

-- | Very basic form. You can add fancy required/optional fields with record modifiers
-- or generate simple textfields from a list of names.
emptyForm :: [T.Text] -> Form
emptyForm extra = Form [] [] (map simpleInput extra) HM.empty HM.empty

-- | Text field builder to be used with 'Form' constructor.
textField :: T.Text -> T.Text -> T.Text -> Input
textField title placeholder help = Input title placeholder help "" False

-- * Processing with HappStack

-- | Fill in form values and errors using 'formData'.
--
-- @
-- -- Use inside 'ServerPart':
-- form <- lookValues checkForm
-- @
lookValues :: Form -> ServerPartT IO Form
lookValues form = do
    reqd <- formData $ required form
    optl <- formData $ optional form
    extr <- formData $ extra form

    return form { formValues = HM.unions $ map HM.fromList [reqd, optl, extr]
                , formErrors = HM.fromList $ checkRequired reqd }

-- | Load assoc list of values from current request.
formData :: [Input] -> ServerPartT IO [(T.Text, [T.Text])]
formData fs = forM fs
    (\field -> do
        let name = T.toLower $ title field
        value <- lookTexts' $ T.unpack name
        return (name, value))

isRequired title = (title, ["This field is required."])
checkRequired = concatMap (\(title, values) -> case values of
                                                   [] -> [isRequired title]
                                                   [""] -> [isRequired title]
                                                   _ -> [])