selectField 函数接受一个 OptionList a 表示从 a 类型的 Haskell 对象列表中选择的内容。 OptionList a 是一个 Option a 值列表,它结合了一个 Text 面向用户的标签,a 值被选中,Text 表示将由客户端返回的 HTML 级值表格。 selectFieldList 函数是一个专门的函数,它使用递增的整数标签来表示 HTML 级别的值,这就是为什么您会看到一系列递增的整数,而不是表单返回的有意义的值。
因此,您想使用selectField 代替selectFieldList。但这还不是故事的结局。据我了解,您正在尝试呈现具有一组动态选择的表单(可能是从数据库查询中单子生成的)。当表单发布时,您希望收到一个有意义的 HTML 级别的值,以便您可以无状态地接受并执行它,而无需记住原始的动态选择集。这样,您就可以绕过runFormPost,直接对返回的值进行操作。
一般来说,这是个坏主意!通过绕过runFormPost,您将绕过跨站点请求伪造 (CSRF) 保护和表单验证。这可能适用于您的特定情况,如果您的表单中只有一个字段,请注意手动验证返回的 HTML 级别的值,并执行您自己的 CSRF 缓解(或在不受信任的上下文中操作一个问题)。但是,一个更通用的解决方案是可能的,虽然它有点 hacky。
让我用一个独立的例子来说明。对于您的动态下拉菜单,每个选项将涉及三个值,Haskell 级别的内部 City 类型(例如,您的 PG.DbCity)和两个 Text 值:出现在下拉菜单,以及一个独立的Key,它将以 HTML 级别的value 属性发送并传回给您以验证并转换回City。
所以,你有,说:
type Key = Text
data City = City { key :: Key, label :: Text } deriving (Show, Eq)
和一组有效的Citys:
validCities = [City "0101" "New York", City "0102" "New Jersey", City "0200" "Newark"]
在现实世界中,City 可以是 persist 数据库实体,您可以使用 Show 实例作为其实体键,并使用其他一些方便的文本字段作为其标签。
我将假设您可以在处理程序中单子生成城市的动态子集(例如,通过数据库查询):
getSomeCities :: Text -> Handler [City]
getSomeCities pfx = return $ filter (pfx `isPrefixOf . label) validCities
并根据完整的城市列表单子验证/查找键(例如,“0101”):
lookupCity :: Key -> Handler (Maybe City)
lookupCity k = return $ find ((== k) . key) validCities
这里值得注意的是,如果你想成为无状态的,你不能根据你发送给客户端的实际选项来验证返回的Key。您只能检查Key 在某些更大的上下文中是否有效(例如,是数据库中的某个有效城市)。从安全的角度来看,您需要为客户可能发布不在您提供的选项中的密钥做好准备。
无论如何,使用selectField 的简单动态下拉菜单可以使用以下形式创建:
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
]
和 GET 处理程序:
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
|]
现在,让我们编写一个标准的 POST 处理程序
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
因为我们使用runFormPost,所以我们对任何其他表单字段都有 CSRF 保护和验证。这里唯一的问题是,由于我们是无国籍的,我们没有可用的城市列表,所以现在我只提供了空列表。
如果你把它粘贴到一个基本的 Yesod 服务器并查看生成的表单的 HTML,你会看到 HTML value 属性是我们可以映射回来的独立键 0101 和 0102去城市。
但是,如果您尝试发布此表单,则会收到错误消息:
无效条目:0101
因为selectField 验证器正在根据一个空的选项列表验证返回的选项。要做的一件简单的事情是在postDropdownR 中提供完整的有效城市集,而不管发送给客户端的城市子集是什么:
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
现在,表单可以正常工作,并回复如下内容:
您选择了选项 City {key = "0102", label = "New Jersey"}
最大的缺点是必须一次性提供全套城市,这对于有效城市的大型数据库来说是不切实际的。
OptionList 类型提供了一些灵活性,因为它的类型包括一个 olOptions :: [Option a] 渲染表单时使用的选项列表和一个单独的函数 olReadExternal :: Text -> Maybe a 用于验证返回的 HTML 级值,但 olReadExternal 仍然是一个纯函数,因此无法在单子上下文中将其作为数据库查询运行。
这就是它变得棘手的地方。我们需要用我们自己的验证器覆盖selectField-生成的Field 的验证代码。这意味着将表单重写为:
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
这里的变化是我们覆盖了Field 中的fieldParse 字段,因此它使用lookupCity monadic 函数进行验证。在postDropDown 中,我们使用空的城市集切换回runFormPosting,因为城市列表根本不用于验证。
一切就绪后,使用下面的代码,您将获得一个单子动态表单,可以使用所有 Yesod 验证和 CSRF 机制进行无状态发布,并且您可以使用您的处理程序单子验证返回的城市自己建造。
完整代码:
{-# 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