diff --git a/Yesod/Form/Fields.hs b/Yesod/Form/Fields.hs index db11563d..741ecbb9 100644 --- a/Yesod/Form/Fields.hs +++ b/Yesod/Form/Fields.hs @@ -17,6 +17,7 @@ module Yesod.Form.Fields , emailField , searchField , selectField + , multiSelectField , AutoFocus , urlField , doubleField @@ -40,6 +41,9 @@ import Network.URI (parseURI) import Database.Persist (PersistField) import Text.HTML.SanitizeXSS (sanitizeBalance) import Control.Monad (when, unless) +import Data.List (intersect, nub) +import Data.Either (rights) +import Data.Maybe (catMaybes) import qualified Blaze.ByteString.Builder.Html.Utf8 as B import Blaze.ByteString.Builder (writeByteString, toLazyByteString) @@ -104,10 +108,12 @@ defaultFormMessage (MsgInvalidBool t) = "Invalid boolean: " `mappend` t defaultFormMessage MsgBoolYes = "Yes" defaultFormMessage MsgBoolNo = "No" -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 +blank :: (Text -> Either msg a) -> [Text] -> Either msg (Maybe a) +blank _ [] = Right Nothing +blank _ ("":_) = Right Nothing +blank f (x:_) = either Left (Right . Just) $ f x + + intField :: (Monad monad, Integral i) => Field (GGWidget master monad ()) FormMessage i intField = Field @@ -115,13 +121,14 @@ intField = Field case Data.Text.Read.signed Data.Text.Read.decimal s of Right (a, "") -> Right a _ -> Left $ MsgInvalidInteger s - , fieldRender = pack . showI + , fieldView = \theId name val isReq -> addHamlet [HAMLET|\ - + |] } where + showVal = either id (pack . showI) showI x = show (fromIntegral x :: Integer) doubleField :: Monad monad => Field (GGWidget master monad ()) FormMessage Double @@ -130,33 +137,34 @@ doubleField = Field case Data.Text.Read.double s of Right (a, "") -> Right a _ -> Left $ MsgInvalidNumber s - , fieldRender = pack . show + , fieldView = \theId name val isReq -> addHamlet [HAMLET|\ - + |] } + where showVal = either id (pack . show) dayField :: Monad monad => Field (GGWidget master monad ()) FormMessage Day dayField = Field { fieldParse = blank $ parseDate . unpack - , fieldRender = pack . show , fieldView = \theId name val isReq -> addHamlet [HAMLET|\ - + |] } + where showVal = either id (pack . show) timeField :: Monad monad => Field (GGWidget master monad ()) FormMessage TimeOfDay timeField = Field { fieldParse = blank $ parseTime . unpack - , fieldRender = pack . show . roundFullSeconds , fieldView = \theId name val isReq -> addHamlet [HAMLET|\ - + |] } where + showVal = either id (pack . show . roundFullSeconds) roundFullSeconds tod = TimeOfDay (todHour tod) (todMin tod) fullSec where @@ -165,12 +173,12 @@ timeField = Field htmlField :: Monad monad => Field (GGWidget master monad ()) FormMessage Html htmlField = Field { fieldParse = blank $ Right . preEscapedString . sanitizeBalance . unpack -- FIXME make changes to xss-sanitize - , fieldRender = pack . renderHtml , fieldView = \theId name val _isReq -> addHamlet [HAMLET|\ -#{val} +#{showVal val} |] } + where showVal = either id (pack . renderHtml) -- | A newtype wrapper around a 'String' that converts newlines to HTML -- br-tags. @@ -192,41 +200,37 @@ instance ToHtml Textarea where textareaField :: Monad monad => Field (GGWidget master monad ()) FormMessage Textarea textareaField = Field - { fieldParse = blank $ Right . Textarea - , fieldRender = unTextarea + { fieldParse = blank $ Right . Textarea , fieldView = \theId name val _isReq -> addHamlet [HAMLET|\ -#{val} +#{either id unTextarea val} |] } hiddenField :: Monad monad => Field (GGWidget master monad ()) FormMessage Text hiddenField = Field { fieldParse = blank $ Right - , fieldRender = id , fieldView = \theId name val _isReq -> addHamlet [HAMLET|\ - + |] } textField :: Monad monad => Field (GGWidget master monad ()) FormMessage Text textField = Field { fieldParse = blank $ Right - , fieldRender = id , fieldView = \theId name val isReq -> [WHAMLET| - + |] } passwordField :: Monad monad => Field (GGWidget master monad ()) FormMessage Text passwordField = Field { fieldParse = blank $ Right - , fieldRender = id , fieldView = \theId name val isReq -> addHamlet [HAMLET|\ - + |] } @@ -274,21 +278,19 @@ emailField = Field \s -> if Email.isValid (unpack s) then Right s else Left $ MsgInvalidEmail s - , fieldRender = id , fieldView = \theId name val isReq -> addHamlet [HAMLET|\ - + |] } type AutoFocus = Bool searchField :: Monad monad => AutoFocus -> Field (GGWidget master monad ()) FormMessage Text searchField autoFocus = Field - { fieldParse = blank Right - , fieldRender = id + { fieldParse = blank Right , fieldView = \theId name val isReq -> do addHtml [HAMLET|\ - + |] when autoFocus $ do addHtml $ [HAMLET|\ @@ -305,10 +307,9 @@ urlField = Field case parseURI $ unpack s of Nothing -> Left $ MsgInvalidUrl s Just _ -> Right s - , fieldRender = id , fieldView = \theId name val isReq -> addHtml [HAMLET| - + |] } @@ -318,6 +319,11 @@ selectField = selectFieldHelper (\_theId _name isSel -> [WHAMLET|_{MsgSelectNone}|]) (\_theId _name value isSel text -> addHtml [HTML|#{text}|]) +multiSelectField :: (Show a, Eq a, Monad monad, RenderMessage master FormMessage) => [(Text, a)] -> Field (GGWidget master (GGHandler sub master monad) ()) FormMessage [a] +multiSelectField = multiSelectFieldHelper + (\theId name inside -> [WHAMLET|^{inside}|]) + (\_theId _name value isSel text -> addHtml [HTML|#{text}|]) + radioField :: (Eq a, Monad monad, RenderMessage master FormMessage) => [(Text, a)] -> Field (GGWidget master (GGHandler sub master monad) ()) FormMessage a radioField = selectFieldHelper (\theId _name inside -> [WHAMLET|^{inside}|]) @@ -334,29 +340,52 @@ radioField = selectFieldHelper boolField :: (Monad monad, RenderMessage master FormMessage) => Field (GGWidget master (GGHandler sub master monad) ()) FormMessage Bool boolField = Field - { fieldParse = \s -> - case s of - Nothing -> Right Nothing - Just "" -> Right Nothing - Just "none" -> Right Nothing - Just "yes" -> Right $ Just True - Just "no" -> Right $ Just False - Just t -> Left $ MsgInvalidBool t - , fieldRender = \a -> if a then "yes" else "no" - , fieldView = \theId name val isReq -> [WHAMLET| -$if not isReq - - _{MsgSelectNone} + { fieldParse = boolParser + , fieldView = \theId name val isReq -> [WHAMLET| + $if not isReq + + _{MsgSelectNone} - + + _{MsgBoolYes} - + _{MsgBoolNo} |] } where - isNone val = not $ val `elem` ["yes", "no"] + boolParser [] = Right Nothing + boolParser (x:_) = case x of + "" -> Right Nothing + "none" -> Right Nothing + "yes" -> Right $ Just True + "no" -> Right $ Just False + t -> Left $ MsgInvalidBool t + showVal = either (\_ -> False) + +multiSelectFieldHelper :: (Show a, Eq a, Monad monad) + => (Text -> Text -> GGWidget master monad () -> GGWidget master monad ()) + -> (Text -> Text -> Text -> Bool -> Text -> GGWidget master monad ()) + -> [(Text, a)] -> Field (GGWidget master monad ()) FormMessage [a] +multiSelectFieldHelper outside inside opts = Field + { fieldParse = selectParser + , fieldView = \theId name vals _ -> + outside theId name $ do + flip mapM_ pairs $ \pair -> inside + theId + name + (pack $ show $ fst pair) + ((fst pair) `elem` (either (\_ -> []) selectedVals vals)) -- We are presuming that select fields can't hold invalid values + (fst $ snd pair) + } + where + pairs = zip [1 :: Int ..] opts -- FIXME use IntMap + rpairs = zip (map snd opts) [1 :: Int ..] + selectedVals vals = map snd $ filter (\y -> fst y `elem` vals) rpairs + selectParser [] = Right Nothing + selectParser xs | not $ null (["", "none"] `intersect` xs) = Right Nothing + | otherwise = (Right . Just . map snd . catMaybes . map (\y -> lookup y pairs) . nub . map fst . rights . map Data.Text.Read.decimal) xs selectFieldHelper :: (Eq a, Monad monad) => (Text -> Text -> GGWidget master monad () -> GGWidget master monad ()) @@ -364,29 +393,29 @@ selectFieldHelper :: (Eq a, Monad monad) -> (Text -> Text -> Text -> Bool -> Text -> GGWidget master monad ()) -> [(Text, a)] -> Field (GGWidget master monad ()) FormMessage a selectFieldHelper outside onOpt inside opts = Field - { fieldParse = \s -> - case s of - Nothing -> Right Nothing - Just "" -> Right Nothing - Just "none" -> Right Nothing - Just x -> - 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 + { fieldParse = selectParser , fieldView = \theId name val isReq -> outside theId name $ do - unless isReq $ onOpt theId name $ not $ val `elem` map (pack . show . fst) pairs + unless isReq $ onOpt theId name $ not $ (render val) `elem` map (pack . show . fst) pairs flip mapM_ pairs $ \pair -> inside theId name (pack $ show $ fst pair) - (val == pack (show $ fst pair)) + ((render val) == pack (show $ fst pair)) (fst $ snd pair) } where pairs = zip [1 :: Int ..] opts -- FIXME use IntMap rpairs = zip (map snd opts) [1 :: Int ..] + render (Left _) = "" + render (Right a) = maybe "" (pack . show) $ lookup a rpairs + selectParser [] = Right Nothing + selectParser (s:_) = case s of + "" -> Right Nothing + "none" -> Right Nothing + x -> 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 diff --git a/Yesod/Form/Functions.hs b/Yesod/Form/Functions.hs index 536c1331..8cae6f62 100644 --- a/Yesod/Form/Functions.hs +++ b/Yesod/Form/Functions.hs @@ -40,7 +40,7 @@ import Yesod.Request (reqNonce, reqWaiRequest, reqGetParams, languages) import Network.Wai (requestMethod) import Text.Hamlet.NonPoly (html) import Data.Monoid (mempty) -import Data.Maybe (fromMaybe) +import Data.Maybe (listToMaybe) import Yesod.Message (RenderMessage (..)) #if __GLASGOW_HASKELL__ >= 700 @@ -104,6 +104,7 @@ mhelper :: (Monad m, RenderMessage master msg, RenderMessage master msg2) -> (a -> FormResult b) -- ^ on success -> Bool -- ^ is it required? -> Form master (GGHandler sub master m) (FormResult b, FieldView xml) + mhelper Field {..} FieldSettings {..} mdef onMissing onFound isReq = do mp <- askParams name <- maybe newFormIdent return fsName @@ -112,16 +113,15 @@ mhelper Field {..} FieldSettings {..} mdef onMissing onFound isReq = do let mr2 = renderMessage master langs let (res, val) = case mp of - Nothing -> (FormMissing, maybe "" fieldRender mdef) + Nothing -> (FormMissing, maybe (Left "") Right mdef) Just p -> - let mval = lookup name p - valB = fromMaybe "" mval - in case fieldParse mval of - Left e -> (FormFailure [renderMessage master langs e], valB) + let mvals = map snd $ filter (\(n,_) -> n == name) p + in case fieldParse mvals of + Left e -> (FormFailure [renderMessage master langs e], maybe (Left "") Left (listToMaybe mvals)) Right mx -> case mx of - Nothing -> (onMissing master langs, valB) - Just x -> (onFound x, valB) + Nothing -> (onMissing master langs, Left "") + Just x -> (onFound x, Right x) return (res, FieldView { fvLabel = toHtml $ mr2 fsLabel , fvTooltip = fmap toHtml $ fmap mr2 fsTooltip diff --git a/Yesod/Form/Input.hs b/Yesod/Form/Input.hs index 2dc36972..cb3d98e5 100644 --- a/Yesod/Form/Input.hs +++ b/Yesod/Form/Input.hs @@ -33,14 +33,16 @@ instance Applicative (FormInput master) where ireq :: (RenderMessage master msg, RenderMessage master FormMessage) => Field (GWidget sub master ()) msg a -> Text -> FormInput master a ireq field name = FormInput $ \m l env -> - case fieldParse field $ lookup name env of - Left e -> Left $ (:) $ renderMessage m l e - Right Nothing -> Left $ (:) $ renderMessage m l $ MsgInputNotFound name - Right (Just a) -> Right a + let filteredEnv = map snd $ filter (\y -> fst y == name) env + in case fieldParse field $ filteredEnv of + Left e -> Left $ (:) $ renderMessage m l e + 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 field name = FormInput $ \m l env -> - case fieldParse field $ lookup name env of + let filteredEnv = map snd $ filter (\y -> fst y == name) env + in case fieldParse field $ filteredEnv of Left e -> Left $ (:) $ renderMessage m l e Right x -> Right x diff --git a/Yesod/Form/Jquery.hs b/Yesod/Form/Jquery.hs index b1a01d22..8e9e4bf9 100644 --- a/Yesod/Form/Jquery.hs +++ b/Yesod/Form/Jquery.hs @@ -63,10 +63,10 @@ class YesodJquery a where urlJqueryUiDateTimePicker :: a -> Either (Route a) Text 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 +blank :: (Text -> Either msg a) -> [Text] -> Either msg (Maybe a) +blank _ [] = Right Nothing +blank _ ("":_) = Right Nothing +blank f (x:_) = either Left (Right . Just) $ f x jqueryDayField :: (YesodJquery master) => JqueryDaySettings -> Field (GWidget sub master ()) FormMessage Day jqueryDayField jds = Field @@ -75,10 +75,9 @@ jqueryDayField jds = Field Right . readMay . unpack - , fieldRender = pack . show , fieldView = \theId name val isReq -> do addHtml [HAMLET|\ - + |] addScript' urlJqueryJs addScript' urlJqueryUiJs @@ -94,6 +93,7 @@ $(function(){$("##{theId}").datepicker({ |] } where + showVal = either id (pack . show) jsBool True = "true" :: Text jsBool False = "false" :: Text mos (Left i) = show i @@ -126,10 +126,9 @@ jqueryDayTimeUTCTime (UTCTime day utcTime) = jqueryDayTimeField :: YesodJquery master => Field (GWidget sub master ()) FormMessage UTCTime jqueryDayTimeField = Field { fieldParse = blank $ parseUTCTime . unpack - , fieldRender = pack . jqueryDayTimeUTCTime , fieldView = \theId name val isReq -> do addHtml [HAMLET|\ - + |] addScript' urlJqueryJs addScript' urlJqueryUiJs @@ -139,6 +138,8 @@ jqueryDayTimeField = Field $(function(){$("##{theId}").datetimepicker({dateFormat : "yyyy/mm/dd h:MM TT"})}); |] } + where + showVal = either id (pack . jqueryDayTimeUTCTime) parseUTCTime :: String -> Either FormMessage UTCTime parseUTCTime s = @@ -152,11 +153,10 @@ parseUTCTime s = jqueryAutocompleteField :: YesodJquery master => Route master -> Field (GWidget sub master ()) FormMessage Text jqueryAutocompleteField src = Field - { fieldParse = Right - , fieldRender = id + { fieldParse = blank $ Right , fieldView = \theId name val isReq -> do addHtml [HAMLET|\ - + |] addScript' urlJqueryJs addScript' urlJqueryUiJs diff --git a/Yesod/Form/Nic.hs b/Yesod/Form/Nic.hs index bbe1ae29..2b8502d5 100644 --- a/Yesod/Form/Nic.hs +++ b/Yesod/Form/Nic.hs @@ -26,15 +26,16 @@ class YesodNic a where urlNicEdit :: a -> Either (Route a) Text 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 +blank :: (Text -> Either msg a) -> [Text] -> Either msg (Maybe a) +blank _ [] = Right Nothing +blank _ ("":_) = Right Nothing +blank f (x:_) = either Left (Right . Just) $ f x + + nicHtmlField :: YesodNic master => Field (GWidget sub master ()) msg Html nicHtmlField = Field { fieldParse = blank $ Right . preEscapedString . sanitizeBalance . unpack -- FIXME - , fieldRender = pack . renderHtml , fieldView = \theId name val _isReq -> do addHtml #if __GLASGOW_HASKELL__ >= 700 @@ -42,7 +43,7 @@ nicHtmlField = Field #else [$hamlet| #endif - #{val} + #{showVal val} |] addScript' urlNicEdit addJulius @@ -54,6 +55,8 @@ nicHtmlField = Field bkLib.onDomLoaded(function(){new nicEditor({fullPanel:true}).panelInstance("#{theId}")}); |] } + where + showVal = either id (pack . renderHtml) addScript' :: (y -> Either (Route y) Text) -> GWidget sub y () addScript' f = do diff --git a/Yesod/Form/Types.hs b/Yesod/Form/Types.hs index 05dedbb4..874c5f93 100644 --- a/Yesod/Form/Types.hs +++ b/Yesod/Form/Types.hs @@ -114,11 +114,10 @@ data FieldView xml = FieldView } data Field xml msg a = Field - { fieldParse :: Maybe Text -> Either msg (Maybe a) - , fieldRender :: a -> Text + { fieldParse :: [Text] -> Either msg (Maybe a) , fieldView :: Text -- ^ ID -> Text -- ^ name - -> Text -- ^ value + -> Either Text a -- ^ value could be invalid text or a legitimate a -> Bool -- ^ required? -> xml } diff --git a/hello-forms.hs b/hello-forms.hs index ccab8f77..cd10b635 100644 --- a/hello-forms.hs +++ b/hello-forms.hs @@ -12,12 +12,14 @@ data Fruit = Apple | Banana | Pear fruits :: [(Text, Fruit)] fruits = map (\x -> (pack $ show x, x)) [minBound..maxBound] -myForm = fixType $ runFormGet $ renderDivs $ pure (,,,,,,) +myForm = fixType $ runFormGet $ renderDivs $ pure (,,,,,,,,) <*> areq boolField "Bool field" Nothing <*> aopt boolField "Opt bool field" Nothing <*> areq textField "Text field" Nothing <*> areq (selectField fruits) "Select field" Nothing <*> aopt (selectField fruits) "Opt select field" Nothing + <*> areq (multiSelectField fruits) "Multi select field" Nothing + <*> aopt (multiSelectField fruits) "Opt multi select field" Nothing <*> aopt intField "Opt int field" Nothing <*> aopt (radioField fruits) "Opt radio" Nothing