haskell - Yesod selectFieldList 返回列表索引号而不是值
问题描述
我正在尝试运行我使用生成的表单selectFieldList
。
data CityContainer = CityContainer (Maybe T.Text)
deriving Show
ambiguityForm :: [PG.DbCity] -> Html -> MForm Handler (FormResult CityContainer, Widget)
ambiguityForm cities = renderDivs $ CityContainer
<$> aopt (selectFieldList cityMap) "City" Nothing
where
cityMap :: [(T.Text, T.Text)]
cityMap = W.mkCityStringM cities
data CityText = CityText T.Text
deriving Show
ambigReciever :: AForm Handler CityText
ambigReciever = CityText
<$> areq textField "City" Nothing
runAmbiguityF
我通过从另一个路由处理程序调用来运行此表单。runAmbiguityF
然后调用postAmbiguityR
.
runAmbiguityF :: [PG.DbCity] -> Handler Html
runAmbiguityF cs = do
(widget, enctype) <- generateFormPost (ambiguityForm cs)
defaultLayout $
[whamlet|
<form method=post action=@{AmbiguityR} enctype=#{enctype}>
^{widget}
<button type="submit">Submit
|]
postAmbiguityR :: Handler Html
postAmbiguityR = do
((result, widget), enctype) <- runFormPost (renderDivs ambigReciever)
case result of --hold :: CityHold
FormSuccess cityHold -> defaultLayout $ [whamlet|#{show cityHold}|]
FormFailure x ->
defaultLayout
[whamlet|
<p>Invalid Input, try again.
<form method=post action=@{AmbiguityR} enctype=#{enctype}>
^{widget}
<button>Submit
|]
当我运行这段代码时,我会得到一个下拉菜单,就像我期望的那样,并且能够提交表单。
我得到一个FormSuccess
,因此显示了CityHold
变量。问题是这个变量不包含cityMap
在ambiguityForm
函数中创建的关联值。相反,我返回了包含在CityText
类型中的列表选择的索引号。
例如说下拉列表有 10 个元素。如果我选择列表的第一个元素,我会回来CityText "1"
。假设我选择了我返回的下拉列表中的最后一项CityText "10"
。
提交表单时如何获取值而不是索引号?
解决方案
该selectField
函数OptionList a
从类型为 的 Haskell 对象列表中获取一个表示选择a
。这OptionList a
是一个Option a
值列表,它结合了Text
面向用户的标签、a
被选择Text
的值以及将由客户端在表单中返回的 HTML 级值。该selectFieldList
函数是对 HTML 级值使用递增整数标签的特化,这就是为什么您会看到一系列递增整数而不是表单返回的有意义值的原因。
所以,你想selectField
用selectFieldList
. 但这还不是故事的结局。据我了解,您正在尝试呈现具有一组动态选择的表单(可能是从数据库查询中单子生成的)。当表单发布时,您希望收到一个有意义的 HTML 级别的值,这样您就可以无状态地接受并执行它,而无需记住原始的动态选择集。这样,您可以绕过runFormPost
并直接处理返回的值。
一般来说,这是一个坏主意! 通过绕过runFormPost
,您将绕过跨站点请求伪造 (CSRF) 保护和表单验证。这可能适用于您的特定情况,如果您的表单中只有一个字段,请注意手动验证返回的 HTML 级别的值,并执行您自己的 CSRF 缓解(或在不受信任的上下文中操作一个问题)。但是,一个更通用的解决方案是可能的,尽管它有点 hacky。
让我用一个独立的例子来说明。对于您的动态下拉菜单,每个选项将涉及三个值,City
Haskell 级别的内部类型(例如,您的 PG.DbCity)和两个Text
值:出现在下拉菜单中的用户可见标签,以及一个自包含Key
的,将在 HTML 级value
属性中发送并传回给您以验证并转换回City
.
所以,你有,说:
type Key = Text
data City = City { key :: Key, label :: Text } deriving (Show, Eq)
和一组有效City
的 s:
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,你会看到 HTMLvalue
属性是自包含的键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
-produced的验证代码。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
这里的变化是我们已经覆盖了中的fieldParse
字段,Field
因此它使用lookupCity
monadic 函数进行验证。在postDropDown
中,我们使用空的城市集切换回runFormPost
ing,因为城市列表根本不用于验证。
有了所有这些,使用下面的代码,您将获得一个单子动态表单,可以使用所有 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
推荐阅读
- sql - 在组内查找范围内的多个值
- node.js - 通过接近度限制socketIO发射到最近的客户端
- excel - 如何在受保护的 VBA 项目中添加引用
- java - Java二叉树实现中链接对象的异常
- php - Laravel Lighthouse GraphQL - 在服务器端排序
- psql - 如何将 pSQL 中的输出文件格式从默认更改为 csv?
- android - Firebird 和 Android JDBC 驱动程序
- swiftui - Swift UI 和画布编辑器
- haskell - (Windows) 从库中插入代码时,PutStrLn 停止工作
- python - 如何在 python 3.7 中修复“运行时错误:迭代期间字典更改大小”