Better support for optional fields

This commit is contained in:
Michael Snoyman 2011-05-20 08:24:30 +03:00
parent 4651ae8b69
commit ab5bf32ea3
6 changed files with 84 additions and 49 deletions

View File

@ -70,6 +70,8 @@ data FormMessage = MsgInvalidInteger Text
| MsgInvalidSecond Text | MsgInvalidSecond Text
| MsgInvalidDay | MsgInvalidDay
| MsgCsrfWarning | MsgCsrfWarning
| MsgValueRequired
| MsgInputNotFound Text
defaultFormMessage :: FormMessage -> Text defaultFormMessage :: FormMessage -> Text
defaultFormMessage (MsgInvalidInteger t) = "Invalid integer: " `mappend` t defaultFormMessage (MsgInvalidInteger t) = "Invalid integer: " `mappend` t
@ -83,10 +85,20 @@ defaultFormMessage (MsgInvalidHour t) = "Invalid hour: " `mappend` t
defaultFormMessage (MsgInvalidMinute t) = "Invalid minute: " `mappend` t defaultFormMessage (MsgInvalidMinute t) = "Invalid minute: " `mappend` t
defaultFormMessage (MsgInvalidSecond t) = "Invalid second: " `mappend` t defaultFormMessage (MsgInvalidSecond t) = "Invalid second: " `mappend` t
defaultFormMessage MsgCsrfWarning = "As a protection against cross-site request forgery attacks, please confirm your form submission." defaultFormMessage MsgCsrfWarning = "As a protection against cross-site request forgery attacks, please confirm your form submission."
defaultFormMessage MsgValueRequired = "Value is required"
defaultFormMessage (MsgInputNotFound t) = "Input not found: " `mappend` t
blank :: (Text -> Either msg a) -> Maybe Text -> Either msg (Maybe a)
blank _ Nothing = Right Nothing
blank _ (Just "") = Right Nothing
blank f (Just t) = either Left (Right . Just) $ f t
intField :: (Monad monad, Integral i) => Field (GGWidget master monad ()) FormMessage i intField :: (Monad monad, Integral i) => Field (GGWidget master monad ()) FormMessage i
intField = Field intField = Field
{ fieldParse = \s -> maybe (Left $ MsgInvalidInteger s) Right . readMayI $ unpack s -- FIXME Data.Text.Read { fieldParse = blank $ \s ->
case Data.Text.Read.signed Data.Text.Read.decimal s of
Right (a, "") -> Right a
_ -> Left $ MsgInvalidInteger s
, fieldRender = pack . showI , fieldRender = pack . showI
, fieldView = \theId name val isReq -> addHamlet , fieldView = \theId name val isReq -> addHamlet
[HAMLET|\ [HAMLET|\
@ -95,13 +107,13 @@ intField = Field
} }
where where
showI x = show (fromIntegral x :: Integer) showI x = show (fromIntegral x :: Integer)
readMayI s = case reads s of
(x, _):_ -> Just $ fromInteger x
[] -> Nothing
doubleField :: Monad monad => Field (GGWidget master monad ()) FormMessage Double doubleField :: Monad monad => Field (GGWidget master monad ()) FormMessage Double
doubleField = Field doubleField = Field
{ fieldParse = \s -> maybe (Left $ MsgInvalidNumber s) Right . readMay $ unpack s -- FIXME use Data.Text.Read { fieldParse = blank $ \s ->
case Data.Text.Read.double s of
Right (a, "") -> Right a
_ -> Left $ MsgInvalidNumber s
, fieldRender = pack . show , fieldRender = pack . show
, fieldView = \theId name val isReq -> addHamlet , fieldView = \theId name val isReq -> addHamlet
[HAMLET|\ [HAMLET|\
@ -111,7 +123,7 @@ doubleField = Field
dayField :: Monad monad => Field (GGWidget master monad ()) FormMessage Day dayField :: Monad monad => Field (GGWidget master monad ()) FormMessage Day
dayField = Field dayField = Field
{ fieldParse = parseDate . unpack { fieldParse = blank $ parseDate . unpack
, fieldRender = pack . show , fieldRender = pack . show
, fieldView = \theId name val isReq -> addHamlet , fieldView = \theId name val isReq -> addHamlet
[HAMLET|\ [HAMLET|\
@ -121,7 +133,7 @@ dayField = Field
timeField :: Monad monad => Field (GGWidget master monad ()) FormMessage TimeOfDay timeField :: Monad monad => Field (GGWidget master monad ()) FormMessage TimeOfDay
timeField = Field timeField = Field
{ fieldParse = parseTime . unpack { fieldParse = blank $ parseTime . unpack
, fieldRender = pack . show . roundFullSeconds , fieldRender = pack . show . roundFullSeconds
, fieldView = \theId name val isReq -> addHamlet , fieldView = \theId name val isReq -> addHamlet
[HAMLET|\ [HAMLET|\
@ -136,7 +148,7 @@ timeField = Field
htmlField :: Monad monad => Field (GGWidget master monad ()) FormMessage Html htmlField :: Monad monad => Field (GGWidget master monad ()) FormMessage Html
htmlField = Field htmlField = Field
{ fieldParse = Right . preEscapedString . sanitizeBalance . unpack -- FIXME make changes to xss-sanitize { fieldParse = blank $ Right . preEscapedString . sanitizeBalance . unpack -- FIXME make changes to xss-sanitize
, fieldRender = pack . renderHtml , fieldRender = pack . renderHtml
, fieldView = \theId name val _isReq -> addHamlet , fieldView = \theId name val _isReq -> addHamlet
[HAMLET|\ [HAMLET|\
@ -164,7 +176,7 @@ instance ToHtml Textarea where
textareaField :: Monad monad => Field (GGWidget master monad ()) FormMessage Textarea textareaField :: Monad monad => Field (GGWidget master monad ()) FormMessage Textarea
textareaField = Field textareaField = Field
{ fieldParse = Right . Textarea { fieldParse = blank $ Right . Textarea
, fieldRender = unTextarea , fieldRender = unTextarea
, fieldView = \theId name val _isReq -> addHamlet , fieldView = \theId name val _isReq -> addHamlet
[HAMLET|\ [HAMLET|\
@ -174,7 +186,7 @@ textareaField = Field
hiddenField :: Monad monad => Field (GGWidget master monad ()) FormMessage Text hiddenField :: Monad monad => Field (GGWidget master monad ()) FormMessage Text
hiddenField = Field hiddenField = Field
{ fieldParse = Right { fieldParse = blank $ Right
, fieldRender = id , fieldRender = id
, fieldView = \theId name val _isReq -> addHamlet , fieldView = \theId name val _isReq -> addHamlet
[HAMLET|\ [HAMLET|\
@ -184,7 +196,7 @@ hiddenField = Field
textField :: Monad monad => Field (GGWidget master monad ()) FormMessage Text textField :: Monad monad => Field (GGWidget master monad ()) FormMessage Text
textField = Field textField = Field
{ fieldParse = Right { fieldParse = blank $ Right
, fieldRender = id , fieldRender = id
, fieldView = \theId name val isReq -> , fieldView = \theId name val isReq ->
[WHAMLET| [WHAMLET|
@ -194,7 +206,7 @@ textField = Field
passwordField :: Monad monad => Field (GGWidget master monad ()) FormMessage Text passwordField :: Monad monad => Field (GGWidget master monad ()) FormMessage Text
passwordField = Field passwordField = Field
{ fieldParse = Right { fieldParse = blank $ Right
, fieldRender = id , fieldRender = id
, fieldView = \theId name val isReq -> addHamlet , fieldView = \theId name val isReq -> addHamlet
[HAMLET|\ [HAMLET|\
@ -242,9 +254,10 @@ parseTimeHelper (h1, h2, m1, m2, s1, s2)
emailField :: Monad monad => Field (GGWidget master monad ()) FormMessage Text emailField :: Monad monad => Field (GGWidget master monad ()) FormMessage Text
emailField = Field emailField = Field
{ fieldParse = \s -> if Email.isValid (unpack s) { fieldParse = blank $
then Right s \s -> if Email.isValid (unpack s)
else Left $ MsgInvalidEmail s then Right s
else Left $ MsgInvalidEmail s
, fieldRender = id , fieldRender = id
, fieldView = \theId name val isReq -> addHamlet , fieldView = \theId name val isReq -> addHamlet
[HAMLET|\ [HAMLET|\
@ -255,7 +268,7 @@ emailField = Field
type AutoFocus = Bool type AutoFocus = Bool
searchField :: Monad monad => AutoFocus -> Field (GGWidget master monad ()) FormMessage Text searchField :: Monad monad => AutoFocus -> Field (GGWidget master monad ()) FormMessage Text
searchField autoFocus = Field searchField autoFocus = Field
{ fieldParse = Right { fieldParse = blank Right
, fieldRender = id , fieldRender = id
, fieldView = \theId name val isReq -> do , fieldView = \theId name val isReq -> do
addHtml [HAMLET|\ addHtml [HAMLET|\
@ -272,9 +285,10 @@ searchField autoFocus = Field
urlField :: Monad monad => Field (GGWidget master monad ()) FormMessage Text urlField :: Monad monad => Field (GGWidget master monad ()) FormMessage Text
urlField = Field urlField = Field
{ fieldParse = \s -> case parseURI $ unpack s of { fieldParse = blank $ \s ->
Nothing -> Left $ MsgInvalidUrl s case parseURI $ unpack s of
Just _ -> Right s Nothing -> Left $ MsgInvalidUrl s
Just _ -> Right s
, fieldRender = id , fieldRender = id
, fieldView = \theId name val isReq -> addHtml , fieldView = \theId name val isReq -> addHtml
[HAMLET| [HAMLET|
@ -283,14 +297,19 @@ urlField = Field
} }
selectField :: (Eq a, Monad monad) => [(Text, a)] -> Field (GGWidget master monad ()) FormMessage a selectField :: (Eq a, Monad monad) => [(Text, a)] -> Field (GGWidget master monad ()) FormMessage a
selectField opts = Field -- FIXME won't work for optional yet selectField opts = Field
{ fieldParse = \s -> { fieldParse = \s ->
case Data.Text.Read.decimal s of case s of
Right (a, "") -> Nothing -> Right Nothing
case lookup a pairs of Just "" -> Right Nothing
Nothing -> Left $ MsgInvalidEntry s Just "none" -> Right Nothing
Just x -> Right $ snd x Just x ->
_ -> Left $ MsgInvalidNumber s case Data.Text.Read.decimal x of
Right (a, "") ->
case lookup a pairs of
Nothing -> Left $ MsgInvalidEntry x
Just y -> Right $ Just $ snd y
_ -> Left $ MsgInvalidNumber x
, fieldRender = \a -> maybe "" (pack . show) $ lookup a rpairs , fieldRender = \a -> maybe "" (pack . show) $ lookup a rpairs
, fieldView = \theId name val isReq -> [WHAMLET| , fieldView = \theId name val isReq -> [WHAMLET|
<select id=#{theId} name=#{name} <select id=#{theId} name=#{name}

View File

@ -27,7 +27,7 @@ module Yesod.Form.Functions
) where ) where
import Yesod.Form.Types import Yesod.Form.Types
import Yesod.Form.Fields (FormMessage (MsgCsrfWarning)) import Yesod.Form.Fields (FormMessage (MsgCsrfWarning, MsgValueRequired))
import Data.Text (Text, pack) import Data.Text (Text, pack)
import Control.Monad.Trans.RWS (ask, get, put, runRWST, tell, evalRWST) import Control.Monad.Trans.RWS (ask, get, put, runRWST, tell, evalRWST)
import Control.Monad.Trans.Class (lift) import Control.Monad.Trans.Class (lift)
@ -86,21 +86,21 @@ askFiles = do
(x, _, _) <- ask (x, _, _) <- ask
return $ liftM snd x return $ liftM snd x
mreq :: (Monad m, RenderMessage master msg, RenderMessage master msg2) mreq :: (Monad m, RenderMessage master msg, RenderMessage master msg2, RenderMessage master FormMessage)
=> Field xml msg a -> FieldSettings msg2 -> Maybe a => Field xml msg a -> FieldSettings msg2 -> Maybe a
-> Form master (GGHandler sub master m) (FormResult a, FieldView xml) -> Form master (GGHandler sub master m) (FormResult a, FieldView xml)
mreq field fs mdef = mhelper field fs mdef (FormFailure ["Value is required"]) FormSuccess True -- TRANS mreq field fs mdef = mhelper field fs mdef (\m l -> FormFailure [renderMessage m l MsgValueRequired]) FormSuccess True
mopt :: (Monad m, RenderMessage master msg, RenderMessage master msg2) mopt :: (Monad m, RenderMessage master msg, RenderMessage master msg2)
=> Field xml msg a -> FieldSettings msg2 -> Maybe (Maybe a) => Field xml msg a -> FieldSettings msg2 -> Maybe (Maybe a)
-> Form master (GGHandler sub master m) (FormResult (Maybe a), FieldView xml) -> Form master (GGHandler sub master m) (FormResult (Maybe a), FieldView xml)
mopt field fs mdef = mhelper field fs (join mdef) (FormSuccess Nothing) (FormSuccess . Just) False mopt field fs mdef = mhelper field fs (join mdef) (const $ const $ FormSuccess Nothing) (FormSuccess . Just) False
mhelper :: (Monad m, RenderMessage master msg, RenderMessage master msg2) mhelper :: (Monad m, RenderMessage master msg, RenderMessage master msg2)
=> Field xml msg a => Field xml msg a
-> FieldSettings msg2 -> FieldSettings msg2
-> Maybe a -> Maybe a
-> FormResult b -- ^ on missing -> (master -> [Text] -> FormResult b) -- ^ on missing
-> (a -> FormResult b) -- ^ on success -> (a -> FormResult b) -- ^ on success
-> Bool -- ^ is it required? -> Bool -- ^ is it required?
-> Form master (GGHandler sub master m) (FormResult b, FieldView xml) -> Form master (GGHandler sub master m) (FormResult b, FieldView xml)
@ -109,15 +109,19 @@ mhelper Field {..} FieldSettings {..} mdef onMissing onFound isReq = do
name <- maybe newFormIdent return fsName name <- maybe newFormIdent return fsName
theId <- lift $ maybe (liftM pack newIdent) return fsId theId <- lift $ maybe (liftM pack newIdent) return fsId
(_, master, langs) <- ask (_, master, langs) <- ask
let mr = renderMessage master langs
let mr2 = renderMessage master langs let mr2 = renderMessage master langs
let (res, val) = let (res, val) =
case mp of case mp of
Nothing -> (FormMissing, maybe "" fieldRender mdef) Nothing -> (FormMissing, maybe "" fieldRender mdef)
Just p -> Just p ->
case fromMaybe "" $ lookup name p of let mval = lookup name p
"" -> (onMissing, "") -- TRANS valB = fromMaybe "" mval
x -> (either (FormFailure . return . mr) onFound $ fieldParse x, x) in case fieldParse mval of
Left e -> (FormFailure [renderMessage master langs e], valB)
Right mx ->
case mx of
Nothing -> (onMissing master langs, valB)
Just x -> (onFound x, valB)
return (res, FieldView return (res, FieldView
{ fvLabel = toHtml $ mr2 fsLabel { fvLabel = toHtml $ mr2 fsLabel
, fvTooltip = fmap toHtml $ fmap mr2 fsTooltip , fvTooltip = fmap toHtml $ fmap mr2 fsTooltip
@ -130,7 +134,7 @@ mhelper Field {..} FieldSettings {..} mdef onMissing onFound isReq = do
, fvRequired = isReq , fvRequired = isReq
}) })
areq :: (Monad m, RenderMessage master msg1, RenderMessage master msg2) areq :: (Monad m, RenderMessage master msg1, RenderMessage master msg2, RenderMessage master FormMessage)
=> Field xml msg1 a -> FieldSettings msg2 -> Maybe a => Field xml msg1 a -> FieldSettings msg2 -> Maybe a
-> AForm ([FieldView xml] -> [FieldView xml]) master (GGHandler sub master m) a -> AForm ([FieldView xml] -> [FieldView xml]) master (GGHandler sub master m) a
areq a b = formToAForm . mreq a b areq a b = formToAForm . mreq a b

View File

@ -1,4 +1,5 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleContexts #-}
module Yesod.Form.Input module Yesod.Form.Input
( FormInput (..) ( FormInput (..)
, runInputGet , runInputGet
@ -8,11 +9,11 @@ module Yesod.Form.Input
) where ) where
import Yesod.Form.Types import Yesod.Form.Types
import Data.Text (Text, append) import Yesod.Form.Fields (FormMessage (MsgInputNotFound))
import Data.Text (Text)
import Control.Applicative (Applicative (..)) import Control.Applicative (Applicative (..))
import Yesod.Handler (GHandler, GGHandler, invalidArgs, runRequestBody, getRequest, getYesod) import Yesod.Handler (GHandler, GGHandler, invalidArgs, runRequestBody, getRequest, getYesod)
import Yesod.Request (reqGetParams, languages) import Yesod.Request (reqGetParams, languages)
import Data.Maybe (fromMaybe)
import Control.Monad (liftM) import Control.Monad (liftM)
import Yesod.Widget (GWidget) import Yesod.Widget (GWidget)
import Yesod.Message (RenderMessage (..)) import Yesod.Message (RenderMessage (..))
@ -30,17 +31,18 @@ instance Applicative (FormInput master) where
(_, Left b) -> Left b (_, Left b) -> Left b
(Right a, Right b) -> Right $ a b (Right a, Right b) -> Right $ a b
ireq :: RenderMessage master msg => Field (GWidget sub master ()) msg a -> Text -> FormInput master a ireq :: (RenderMessage master msg, RenderMessage master FormMessage) => Field (GWidget sub master ()) msg a -> Text -> FormInput master a
ireq field name = FormInput $ \m l env -> ireq field name = FormInput $ \m l env ->
case lookup name env of case fieldParse field $ lookup name env of
Nothing -> Left $ (:) $ append "Input not found: " name -- TRANS Left e -> Left $ (:) $ renderMessage m l e
Just x -> either (Left . (:) . renderMessage m l) Right $ fieldParse field x Right Nothing -> Left $ (:) $ renderMessage m l $ MsgInputNotFound name
Right (Just a) -> Right a
iopt :: RenderMessage master msg => Field (GWidget sub master ()) msg a -> Text -> FormInput master (Maybe a) iopt :: RenderMessage master msg => Field (GWidget sub master ()) msg a -> Text -> FormInput master (Maybe a)
iopt field name = FormInput $ \m l env -> iopt field name = FormInput $ \m l env ->
case fromMaybe "" $ lookup name env of case fieldParse field $ lookup name env of
"" -> Right Nothing Left e -> Left $ (:) $ renderMessage m l e
x -> either (Left . (:) . renderMessage m l) (Right . Just) $ fieldParse field x Right x -> Right x
runInputGet :: Monad monad => FormInput master a -> GGHandler sub master monad a runInputGet :: Monad monad => FormInput master a -> GGHandler sub master monad a
runInputGet (FormInput f) = do runInputGet (FormInput f) = do

View File

@ -63,9 +63,14 @@ class YesodJquery a where
urlJqueryUiDateTimePicker :: a -> Either (Route a) Text urlJqueryUiDateTimePicker :: a -> Either (Route a) Text
urlJqueryUiDateTimePicker _ = Right "http://github.com/gregwebs/jquery.ui.datetimepicker/raw/master/jquery.ui.datetimepicker.js" urlJqueryUiDateTimePicker _ = Right "http://github.com/gregwebs/jquery.ui.datetimepicker/raw/master/jquery.ui.datetimepicker.js"
blank :: (Text -> Either msg a) -> Maybe Text -> Either msg (Maybe a)
blank _ Nothing = Right Nothing
blank _ (Just "") = Right Nothing
blank f (Just t) = either Left (Right . Just) $ f t
jqueryDayField :: (YesodJquery master) => JqueryDaySettings -> Field (GWidget sub master ()) FormMessage Day jqueryDayField :: (YesodJquery master) => JqueryDaySettings -> Field (GWidget sub master ()) FormMessage Day
jqueryDayField jds = Field jqueryDayField jds = Field
{ fieldParse = maybe { fieldParse = blank $ maybe
(Left MsgInvalidDay) (Left MsgInvalidDay)
Right Right
. readMay . readMay
@ -120,7 +125,7 @@ jqueryDayTimeUTCTime (UTCTime day utcTime) =
jqueryDayTimeField :: YesodJquery master => Field (GWidget sub master ()) FormMessage UTCTime jqueryDayTimeField :: YesodJquery master => Field (GWidget sub master ()) FormMessage UTCTime
jqueryDayTimeField = Field jqueryDayTimeField = Field
{ fieldParse = parseUTCTime . unpack { fieldParse = blank $ parseUTCTime . unpack
, fieldRender = pack . jqueryDayTimeUTCTime , fieldRender = pack . jqueryDayTimeUTCTime
, fieldView = \theId name val isReq -> do , fieldView = \theId name val isReq -> do
addHtml [HAMLET|\ addHtml [HAMLET|\

View File

@ -26,9 +26,14 @@ class YesodNic a where
urlNicEdit :: a -> Either (Route a) Text urlNicEdit :: a -> Either (Route a) Text
urlNicEdit _ = Right "http://js.nicedit.com/nicEdit-latest.js" urlNicEdit _ = Right "http://js.nicedit.com/nicEdit-latest.js"
blank :: (Text -> Either msg a) -> Maybe Text -> Either msg (Maybe a)
blank _ Nothing = Right Nothing
blank _ (Just "") = Right Nothing
blank f (Just t) = either Left (Right . Just) $ f t
nicHtmlField :: YesodNic master => Field (GWidget sub master ()) msg Html nicHtmlField :: YesodNic master => Field (GWidget sub master ()) msg Html
nicHtmlField = Field nicHtmlField = Field
{ fieldParse = Right . preEscapedString . sanitizeBalance . unpack -- FIXME { fieldParse = blank $ Right . preEscapedString . sanitizeBalance . unpack -- FIXME
, fieldRender = pack . renderHtml , fieldRender = pack . renderHtml
, fieldView = \theId name val _isReq -> do , fieldView = \theId name val _isReq -> do
addHtml addHtml

View File

@ -113,7 +113,7 @@ data FieldView xml = FieldView
} }
data Field xml msg a = Field data Field xml msg a = Field
{ fieldParse :: Text -> Either msg a -- FIXME probably want to make this more sophisticated, handle no form, no field { fieldParse :: Maybe Text -> Either msg (Maybe a)
, fieldRender :: a -> Text , fieldRender :: a -> Text
, fieldView :: Text -- ^ ID , fieldView :: Text -- ^ ID
-> Text -- ^ name -> Text -- ^ name