Add selectFieldGrouped

This commit is contained in:
Jakob Schöttl 2021-04-10 11:42:43 +02:00
parent daf977fdb1
commit 993de7fa86

View File

@ -45,6 +45,7 @@ module Yesod.Form.Fields
, selectFieldHelper , selectFieldHelper
, selectField , selectField
, selectFieldList , selectFieldList
, selectFieldListGrouped
, radioField , radioField
, radioFieldList , radioFieldList
, checkboxesField , checkboxesField
@ -54,6 +55,7 @@ module Yesod.Form.Fields
, Option (..) , Option (..)
, OptionList (..) , OptionList (..)
, mkOptionList , mkOptionList
, mkOptionListGrouped
, optionsPersist , optionsPersist
, optionsPersistKey , optionsPersistKey
, optionsPairs , optionsPairs
@ -427,7 +429,13 @@ selectFieldList :: (Eq a, RenderMessage site FormMessage, RenderMessage site msg
-> Field (HandlerFor site) a -> Field (HandlerFor site) a
selectFieldList = selectField . optionsPairs selectFieldList = selectField . optionsPairs
-- | Creates a @\<select>@ tag for selecting one option. Example usage: -- | Creates a @\<select>@ tag with @\<optgroup>@s for selecting one option.
selectFieldListGrouped :: (Eq a, RenderMessage site FormMessage, RenderMessage site msg)
=> [(msg, [(msg, a)])]
-> Field (HandlerFor site) a
selectFieldListGrouped = selectField . optionsPairsGrouped
-- | Creates a @\<select>@ tag with optional @\<optgroup>@s for selecting one option. Example usage:
-- --
-- > areq (selectField $ optionsPairs [(MsgValue1, "value1"),(MsgValue2, "value2")]) "Which value?" Nothing -- > areq (selectField $ optionsPairs [(MsgValue1, "value1"),(MsgValue2, "value2")]) "Which value?" Nothing
selectField :: (Eq a, RenderMessage site FormMessage) selectField :: (Eq a, RenderMessage site FormMessage)
@ -446,6 +454,9 @@ $newline never
$newline never $newline never
<option value=#{value} :isSel:selected>#{text} <option value=#{value} :isSel:selected>#{text}
|]) -- inside |]) -- inside
(Just $ \label -> [whamlet|
<optgroup label=#{label}>
|]) -- group label
-- | Creates a @\<select>@ tag for selecting multiple options. -- | Creates a @\<select>@ tag for selecting multiple options.
multiSelectFieldList :: (Eq a, RenderMessage site msg) multiSelectFieldList :: (Eq a, RenderMessage site msg)
@ -531,6 +542,7 @@ $newline never
<input id=#{theId}-#{value} type=radio name=#{name} value=#{value} :isSel:checked *{attrs}> <input id=#{theId}-#{value} type=radio name=#{name} value=#{value} :isSel:checked *{attrs}>
\#{text} \#{text}
|]) |])
Nothing
-- | Creates a group of radio buttons to answer the question given in the message. Radio buttons are used to allow differentiating between an empty response (@Nothing@) and a no response (@Just False@). Consider using the simpler 'checkBoxField' if you don't need to make this distinction. -- | Creates a group of radio buttons to answer the question given in the message. Radio buttons are used to allow differentiating between an empty response (@Nothing@) and a no response (@Just False@). Consider using the simpler 'checkBoxField' if you don't need to make this distinction.
-- --
@ -598,10 +610,20 @@ $newline never
showVal = either (\_ -> False) showVal = either (\_ -> False)
-- | A structure holding a list of options. Typically you can use a convenience function like 'mkOptionList' or 'optionsPairs' instead of creating this directly. -- | A structure holding a list of options. Typically you can use a convenience function like 'mkOptionList' or 'optionsPairs' instead of creating this directly.
data OptionList a = OptionList data OptionList a
= OptionList
{ olOptions :: [Option a] { olOptions :: [Option a]
, olReadExternal :: Text -> Maybe a -- ^ A function mapping from the form's value ('optionExternalValue') to the selected Haskell value ('optionInternalValue'). , olReadExternal :: Text -> Maybe a -- ^ A function mapping from the form's value ('optionExternalValue') to the selected Haskell value ('optionInternalValue').
} }
| OptionListGrouped
{ olOptionsGrouped :: [(Text, [Option a])]
, olReadExternalGrouped :: Text -> Maybe a -- ^ A function mapping from the form's value ('optionExternalValue') to the selected Haskell value ('optionInternalValue').
}
-- | Convert grouped 'OptionList' to a normal one.
flattenOptionList :: OptionList a -> OptionList a
flattenOptionList (OptionListGrouped os re) = OptionList (concatMap snd os) re
flattenOptionList ol = ol
-- | Since 1.4.6 -- | Since 1.4.6
instance Functor OptionList where instance Functor OptionList where
@ -615,6 +637,13 @@ mkOptionList os = OptionList
, olReadExternal = flip Map.lookup $ Map.fromList $ map (optionExternalValue &&& optionInternalValue) os , olReadExternal = flip Map.lookup $ Map.fromList $ map (optionExternalValue &&& optionInternalValue) os
} }
-- | Creates an 'OptionList', using a 'Map' to implement the 'olReadExternalGrouped' function.
mkOptionListGrouped :: [(Text, [Option a])] -> OptionList a
mkOptionListGrouped os = OptionListGrouped
{ olOptionsGrouped = os
, olReadExternalGrouped = flip Map.lookup $ Map.fromList $ map (optionExternalValue &&& optionInternalValue) $ concatMap snd os
}
data Option a = Option data Option a = Option
{ optionDisplay :: Text -- ^ The user-facing label. { optionDisplay :: Text -- ^ The user-facing label.
, optionInternalValue :: a -- ^ The Haskell value being selected. , optionInternalValue :: a -- ^ The Haskell value being selected.
@ -637,6 +666,28 @@ optionsPairs opts = do
} }
return $ mkOptionList (zipWith mkOption [1 :: Int ..] opts) return $ mkOptionList (zipWith mkOption [1 :: Int ..] opts)
-- | Creates an 'OptionList' from a list of (display-value, internal value) pairs.
optionsPairsGrouped
:: (MonadHandler m, RenderMessage (HandlerSite m) msg)
=> [(msg, [(msg, a)])] -> m (OptionList a)
optionsPairsGrouped opts = do
mr <- getMessageRender
let mkOption (external, (display, internal)) =
Option { optionDisplay = mr display
, optionInternalValue = internal
, optionExternalValue = pack $ show external
}
opts' = enumerateSublists opts -- :: [(grp, [(Int, (msg, a))])]
opts'' = map (\(x, ys) -> (mr x, map mkOption ys)) opts'
return $ mkOptionListGrouped opts''
-- | Helper to enumerate sublists with one consecutive index.
enumerateSublists :: [(a, [b])] -> [(a, [(Int, b)])]
enumerateSublists xss =
let --yss :: [(Int, (a, [b]))]
yss = snd $ foldl (\(i, res) xs -> (i + (length.snd) xs, res ++ [(i, xs)])) (1, []) xss
in map (\(i, (x, ys)) -> (x, zip [i :: Int ..] ys)) yss
-- | Creates an 'OptionList' from an 'Enum', using its 'Show' instance for the user-facing value. -- | Creates an 'OptionList' from an 'Enum', using its 'Show' instance for the user-facing value.
optionsEnum :: (MonadHandler m, Show a, Enum a, Bounded a) => m (OptionList a) optionsEnum :: (MonadHandler m, Show a, Enum a, Bounded a) => m (OptionList a)
optionsEnum = optionsPairs $ map (\x -> (pack $ show x, x)) [minBound..maxBound] optionsEnum = optionsPairs $ map (\x -> (pack $ show x, x)) [minBound..maxBound]
@ -731,7 +782,7 @@ optionsPersistKey filts ords toDisplay = fmap mkOptionList $ do
}) pairs }) pairs
-- | -- |
-- A helper function for constucting 'selectField's. You may want to use this when you define your custom 'selectField's or 'radioField's. -- A helper function for constucting 'selectField's with optional option groups. You may want to use this when you define your custom 'selectField's or 'radioField's.
-- --
-- @since 1.6.2 -- @since 1.6.2
selectFieldHelper selectFieldHelper
@ -739,23 +790,26 @@ selectFieldHelper
=> (Text -> Text -> [(Text, Text)] -> WidgetFor site () -> WidgetFor site ()) -- ^ Outermost part of the field => (Text -> Text -> [(Text, Text)] -> WidgetFor site () -> WidgetFor site ()) -- ^ Outermost part of the field
-> (Text -> Text -> Bool -> WidgetFor site ()) -- ^ An option for None if the field is optional -> (Text -> Text -> Bool -> WidgetFor site ()) -- ^ An option for None if the field is optional
-> (Text -> Text -> [(Text, Text)] -> Text -> Bool -> Text -> WidgetFor site ()) -- ^ Other options -> (Text -> Text -> [(Text, Text)] -> Text -> Bool -> Text -> WidgetFor site ()) -- ^ Other options
-> (Maybe (Text -> WidgetFor site ())) -- ^ Group headers placed inbetween options
-> HandlerFor site (OptionList a) -> HandlerFor site (OptionList a)
-> Field (HandlerFor site) a -> Field (HandlerFor site) a
selectFieldHelper outside onOpt inside opts' = Field selectFieldHelper outside onOpt inside grpHdr opts' = Field
{ fieldParse = \x _ -> do { fieldParse = \x _ -> do
opts <- opts' opts <- fmap flattenOptionList opts'
return $ selectParser opts x return $ selectParser opts x
, fieldView = \theId name attrs val isReq -> do , fieldView = \theId name attrs val isReq -> do
opts <- fmap olOptions $ handlerToWidget opts'
outside theId name attrs $ do outside theId name attrs $ do
unless isReq $ onOpt theId name $ not $ render opts val `elem` map optionExternalValue opts optsFlat <- fmap (olOptions.flattenOptionList) $ handlerToWidget opts'
forM_ opts $ \opt -> inside unless isReq $ onOpt theId name $ not $ render optsFlat val `elem` map optionExternalValue optsFlat
theId opts'' <- handlerToWidget opts'
name case opts'' of
((if isReq then (("required", "required"):) else id) attrs) (OptionList{}) -> constructOptions theId name attrs val isReq optsFlat
(optionExternalValue opt) (OptionListGrouped{olOptionsGrouped=grps}) -> do
((render opts val) == optionExternalValue opt) forM_ grps $ \(grp, opts) -> do
(optionDisplay opt) case grpHdr of
Just hdr -> hdr grp
Nothing -> return ()
constructOptions theId name attrs val isReq opts
, fieldEnctype = UrlEncoded , fieldEnctype = UrlEncoded
} }
where where
@ -768,6 +822,14 @@ selectFieldHelper outside onOpt inside opts' = Field
x -> case olReadExternal opts x of x -> case olReadExternal opts x of
Nothing -> Left $ SomeMessage $ MsgInvalidEntry x Nothing -> Left $ SomeMessage $ MsgInvalidEntry x
Just y -> Right $ Just y Just y -> Right $ Just y
constructOptions theId name attrs val isReq opts =
forM_ opts $ \opt -> inside
theId
name
((if isReq then (("required", "required"):) else id) attrs)
(optionExternalValue opt)
((render opts val) == optionExternalValue opt)
(optionDisplay opt)
-- | Creates an input with @type="file"@. -- | Creates an input with @type="file"@.
fileField :: Monad m fileField :: Monad m