The selectField function takes an OptionList a representing a selection from a list of Haskell objects of type a. The OptionList a is a list of Option a values that combine a Text user-facing label, the a value being selected, and the Text for the HTML-level value that will be returned by the client in the form. The selectFieldList function is a specialization that uses increasing integer labels for the HTML-level values, which is why you are seeing a series of increasing integers instead of meaningful values returned by your form.
So, you want to use selectField in place of selectFieldList. That's not the end of the story though. As I understand it, you are trying to render a form with a dynamic set of choices (presumably generated monadically from a database query). When the form is posted, you are hoping to receive a meaningful HTML-level value, so that you can accept and act on it statelessly without needing to remember the original dynamic set of choices. That way, you can bypass runFormPost and act on the returned value directly.
In general, this is a bad idea! By bypassing runFormPost, you are bypassing cross-site request forgery (CSRF) protection and form validation. This might work for your specific case, if you have only a single field in your form, take care to validate the returned HTML-level value manually, and do your own CSRF mitigation (or are operating in a trusted context where this isn't an issue). But, a more general solution is possible, though it's a little hacky.
Let me use a self-contained example to illustrate. For your dynamic drop down, for each option there will be three values involved, the internal City type at the Haskell level (e.g., your PG.DbCity), and two Text values: a user-visible label that appears in the drop down menu, and a self-contained Key that will be sent in HTML-level value attributes and passed back to you to validate and convert back into a City.
So, you've got, say:
type Key = Text
data City = City { key :: Key, label :: Text } deriving (Show, Eq)
and a set of valid Citys:
validCities = [City "0101" "New York", City "0102" "New Jersey", City "0200" "Newark"]
In the real world, City can be a persist database entity, and you can use the Show instance for its entity key and some other handy text field for its label.
I'm going to assume you can monadically generate a dynamic subset of cities in a handler (e.g., through a database query):
getSomeCities :: Text -> Handler [City]
getSomeCities pfx = return $ filter (pfx `isPrefixOf . label) validCities
and monadically validate/lookup a key (e.g., "0101") against the full list of cities:
lookupCity :: Key -> Handler (Maybe City)
lookupCity k = return $ find ((== k) . key) validCities
It's worth noting here that, if you want to be stateless, you can't realistically validate the returned Key against the actual options you sent to the client. You can only check that the Key is valid in some larger context (e.g., is some valid city in the database). From a security point of view, you need to be prepared for the possibility that a client could post a key that was not among the options you provided.
Anyway, a simple dynamic dropdown using selectField might be created with the form:
dropDownForm :: [City] -> Html -> MForm Handler (FormResult City, Widget)
dropDownForm cities = renderDivs $
areq (selectField ol) "" Nothing
where ol :: Handler (OptionList City)
ol = do
mr <- getMessageRender
return $ mkOptionList [ Option (mr lbl) city key
| city@(City key lbl) <- cities
]
and the GET handler:
getDropdownR :: Handler Html
getDropdownR = do
-- some dynamic subset of the valid cities
cities <- getSomeCities "New "
(widget, enctype) <- generateFormPost (dropDownForm cities)
defaultLayout [whamlet|
<form method=post action=@{DropdownR} enctype=#{enctype}>
^{widget}
<button>Submit
|]
For now, let's write a standard POST handler
postDropdownR :: Handler Html
postDropdownR = do
((result, _), _) <- runFormPost (dropDownForm [])
case result of
FormSuccess opt -> do
setMessage . toHtml $ "You chose option " <> show opt
FormFailure txt -> do
setMessage (toHtml $ Text.unlines txt)
redirect DropdownR
Because we use runFormPost, we have CSRF protection and validation for any other form fields. The only issue here is that, since we are stateless, we don't have the list of cities available, so for now I've just provided the empty list.
If you stick this into a basic Yesod server and view the HTML of the generated form, you'll see that the HTML value attributes are the self-contained keys 0101 and 0102 that we can map back to the cities.
However, if you try to POST this form, you'll get back an error:
Invalid entry: 0101
because the selectField validator is validating the returned option against an empty list of options. One simple thing to do would be to supply the full set of valid cities in postDropdownR, regardless of the subset of cities sent to the client:
postDropdownR' :: Handler Html
postDropdownR' = do
((result, _), _) <- runFormPost (dropDownForm' validCities) -- CHANGE HERE
case result of
FormSuccess opt -> do
setMessage . toHtml $ "You chose option " <> show opt
FormFailure txt -> do
setMessage (toHtml $ Text.unlines txt)
redirect DropdownR
Now, the form works fine and responds with something like:
You chose option City {key = "0102", label = "New Jersey"}
The big disadvantage is that the full set of cities must be supplied all at once, which is not going to be practical for a large database of valid cities.
The OptionList type provides some flexibility in that its type includes an olOptions :: [Option a] list of options used when rendering the form and a separate function olReadExternal :: Text -> Maybe a for validating the HTML-level value returned, but olReadExternal is still a pure function, so there's no way to run it as a database query in a monadic context.
This is where it gets hacky. We need to override the validation code for the selectField-produced Field with our own validator. This means rewriting the form as:
dropDownForm :: [City] -> Html -> MForm Handler (FormResult City, Widget)
dropDownForm cities = renderDivs $
areq (selectField' ol) "" Nothing
where ol :: Handler (OptionList City)
ol = do
mr <- getMessageRender
return $ mkOptionList [ Option (mr lbl) city key
| city@(City key lbl) <- cities
]
selectField' :: Handler (OptionList City) -> Field Handler City
selectField' ol = (selectField ol) { fieldParse = fp }
-- adapted from `selectParser` in Yesod.Form.Fields source
fp :: [Text] -> [FileInfo] -> Handler (Either (SomeMessage Site) (Maybe City))
-- apparently, there are several ways of selecting nothing
fp [] _ = return $ Right Nothing
fp ("none":_) _ = return $ Right Nothing
fp ("":_) _ = return $ Right Nothing
-- if you have a City key, you need to validate it
fp (x:_) _ = Right <$> lookupCity x
The change here is that we've overridden the fieldParse field in the Field so it validates using the lookupCity monadic function. In postDropDown, we switch back to runFormPosting using an empty set of cities, because the list of cities isn't used at all for validation.
With all that in place, using the code below, you get a monadically dynamic form that can be statelessly posted with the all the Yesod validation and CSRF mechanisms in place, and you can monadically validate the returned city using a handler of your own construction.
The full code:
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
import Yesod hiding (Key)
import Data.Text (Text)
import Data.List (find)
import qualified Data.Text as Text
import Data.Coerce
data Site = Site
mkYesod "Site" [parseRoutes|
/ DropdownR GET POST
|]
instance Yesod Site
instance RenderMessage Site FormMessage where
renderMessage _ _ = defaultFormMessage
type Key = Text
data City = City { key :: Key, label :: Text } deriving (Show, Eq)
validCities = [City "0101" "New York", City "0102" "New Jersey", City "0200" "Newark"]
getSomeCities :: Text -> Handler [City]
getSomeCities pfx = return $ filter (Text.isPrefixOf pfx . label) validCities
lookupCity :: Key -> Handler (Maybe City)
lookupCity k = return $ find ((== k) . key) validCities
dropDownForm :: [City] -> Html -> MForm Handler (FormResult City, Widget)
dropDownForm cities = renderDivs $
areq (selectField' ol) "" Nothing
where ol :: Handler (OptionList City)
ol = do
mr <- getMessageRender
return $ mkOptionList [ Option (mr lbl) city key
| city@(City key lbl) <- cities
]
selectField' :: Handler (OptionList City) -> Field Handler City
selectField' ol = (selectField ol) { fieldParse = fp }
-- adapted from `selectParser` in Yesod.Form.Fields source
fp :: [Text] -> [FileInfo] -> Handler (Either (SomeMessage Site) (Maybe City))
-- apparently, there are several ways of selecting nothing
fp [] _ = return $ Right Nothing
fp ("none":_) _ = return $ Right Nothing
fp ("":_) _ = return $ Right Nothing
-- if you have a City key, you need to validate it
fp (x:_) _ = Right <$> lookupCity x
getDropdownR :: Handler Html
getDropdownR = do
-- some dynamic subset of the valid cities
cities <- getSomeCities "New "
(widget, enctype) <- generateFormPost (dropDownForm cities)
defaultLayout [whamlet|
<form method=post action=@{DropdownR} enctype=#{enctype}>
^{widget}
<button>Submit
|]
postDropdownR :: Handler Html
postDropdownR = do
((result, _), _) <- runFormPost (dropDownForm []) -- empty city list ignored
case result of
FormSuccess opt -> do
setMessage . toHtml $ "You chose option " <> show opt
FormFailure txt -> do
setMessage (toHtml $ Text.unlines txt)
redirect DropdownR
main :: IO ()
main = warp 3000 Site