我们已经提到了边界问题:每当数据进入或离开应用程序时,我们都需要对其进行验证。可能最困难的地方就是表单。表单代码编写很复杂;在理想的情况下中,我们想要一个解决以下问题的解决方案:
- 确保数据有效。
- 将原始的字符串数据提交到Haskell的数据类型。
- 生成用于显示表单的HTML代码。
- 生成Javascript以进行客户端验证并提供更加用户友好的小部件,例如日期选择器。
- 通过将简单的表单组合在一起来构建更复杂的表单。
- 自动为我们的字段指定名称以保证名称的唯一性。
yesod-form包用一种简单的声明式的API提供了这些功能。它建立在Yesod的小部件之上,以简化表单的样式并适当地应用Javascript。和Yesod的其他部分一样,它使用Haskell的类型系统来保证程序的正确性。
概要
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
import Control.Applicative ((<$>), (<*>))
import Data.Text (Text)
import Data.Time (Day)
import Yesod
import Yesod.Form.Jquery
data App = App
mkYesod "App" [parseRoutes|
/ HomeR GET
/person PersonR POST
|]
instance Yesod App
-- Tells our application to use the standard English messages.
-- If you want i18n, then you can supply a translating function instead.
instance RenderMessage App FormMessage where
renderMessage _ _ = defaultFormMessage
-- And tell us where to find the jQuery libraries. We'll just use the defaults,
-- which point to the Google CDN.
instance YesodJquery App
-- The datatype we wish to receive from the form
data Person = Person
{ personName :: Text
, personBirthday :: Day
, personFavoriteColor :: Maybe Text
, personEmail :: Text
, personWebsite :: Maybe Text
}
deriving Show
-- Declare the form. The type signature is a bit intimidating, but here's the
-- overview:
--
-- * The Html parameter is used for encoding some extra information. See the
-- discussion regarding runFormGet and runFormPost below for further
-- explanation.
--
-- * We have our Handler as the inner monad, which indicates which site this is
-- running in.
--
-- * FormResult can be in three states: FormMissing (no data available),
-- FormFailure (invalid data) and FormSuccess
--
-- * The Widget is the viewable form to place into the web page.
--
-- Note that the scaffolded site provides a convenient Form type synonym,
-- so that our signature could be written as:
--
-- > personForm :: Form Person
--
-- For our purposes, it's good to see the long version.
personForm :: Html -> MForm Handler (FormResult Person, Widget)
personForm = renderDivs $ Person
<$> areq textField "Name" Nothing
<*> areq (jqueryDayField def
{ jdsChangeYear = True -- give a year dropdown
, jdsYearRange = "1900:-5" -- 1900 till five years ago
}) "Birthday" Nothing
<*> aopt textField "Favorite color" Nothing
<*> areq emailField "Email address" Nothing
<*> aopt urlField "Website" Nothing
-- The GET handler displays the form
getHomeR :: Handler Html
getHomeR = do
-- Generate the form to be displayed
(widget, enctype) <- generateFormPost personForm
defaultLayout
[whamlet|
<p>
The widget generated contains only the contents
of the form, not the form tag itself. So...
<form method=post action=@{PersonR} enctype=#{enctype}>
^{widget}
<p>It also doesn't include the submit button.
<button>Submit
|]
-- The POST handler processes the form. If it is successful, it displays the
-- parsed person. Otherwise, it displays the form again with error messages.
postPersonR :: Handler Html
postPersonR = do
((result, widget), enctype) <- runFormPost personForm
case result of
FormSuccess person -> defaultLayout [whamlet|<p>#{show person}|]
_ -> defaultLayout
[whamlet|
<p>Invalid input, let's try again.
<form method=post action=@{PersonR} enctype=#{enctype}>
^{widget}
<button>Submit
|]
main :: IO ()
main = warp 3000 App
Kinds of Forms
在进入类型本身之前,我们首先介绍三种不同类型的表单。
Applicative (应用函子)
这是最常用的(它就是在概要demo中出现的那个)。Applicative为我们提供了一些很好的方法,让错误消息可以合并在一起并保持一种非常高级的声明性方法。 (有关Applicative代码的更多信息,请参阅Haskell wiki。)
Monadic
是比Applicative更强大的替代方案。虽然这可以让您获得更大的灵活性,但这样做的代价是更加冗长。如果要创建不符合标准双栏外观的表单,则非常有用。
Input
仅用于接收输入。不生成用于接收用户输入的任何HTML。用于与现有表单进行交互。
此外,您需要设置的每个表单和字段都有许多不同的变量:
- 该字段是必需的还是可选的?
- 是通过GET或POST提交?
- 它是否有默认值?
最重要的目标是最小化字段定义的数量,并让它们在尽可能多的上下文中工作。这样做的一个结果是我们最终会为每个字段添加一些额外的单词。在概要中,您可能已经注意到了诸如areq和额外的Nothing参数之类的东西。我们将在本章的过程中介绍为什么所有这些都存在,但是现在意识到通过使这些参数显式化,我们能够以许多不同的方式重用个体字段(如intField)
关于命名约定的快速说明。每种表单类型都有一个单字母前缀(A,M和I),用于少数几个地方,例如说MForm。我们还使用req和opt来表示必需和可选。结合这些,我们用areq创建一个必需的应用字段,或者用iopt创建一个可选的输入字段。
Types
Yesod.Form.Types模块声明了一些类型。我们不会涵盖所有可用的类型,而是将重点放在最重要的类型上。让我们从一些简单的开始:
Enctype
编码类型,是UrlEncoded或者Multipart。这个数据是ToHtml
的实例,所以你可以直接在Hamlet中使用enctype。
FormResult
有三种可能的状态,没有提交数据是FormMissing
,如果解析表单时出错(例如,缺少必填字段,内容无效),是FormFailure
,如果一切顺利就是FormSuccess。
FormMessage
表示可以作为数据类型的所有不同消息。例如,使用MsgInvalidInteger来表示提供的值不是整数。通过保持这种高度结构化的数据,您可以提供任何类型的渲染功能,从而实现应用程序的国际化(i18n)。 接下来,我们有一些数据类型用于定义单个字段。我们将字段定义为单个信息,例如数字,字符串或电子邮件地址。字段组合在一起从而构成表单。
Field
定义两个功能:如何将用户的文本输入解析为Haskell值,以及如何创建要显示给用户的Widget。 yesod-form在Yesod.Form.Fields中定义了许多单独的字段。
FieldSettings
表示有关如何显示字段的基本信息,例如显示名称,可选的提示以及可能的硬编码ID和name
属性如果未提供,则会自动生成它们)。请注意,FieldSettings提供了一个IsString实现,因此当您需要提供FieldSettings值时,您实际上可以输入文字字符串。这就是我们在概要中与它进行互动的方式。
最后,我们得到了重要的东西:表单本身。有三种类型:MForm用于monadic形式,AForm用于applicative,FormInput用于input。MForm实际上是monad堆栈的类型同义词,它提供以下功能:
- Reader monad为我们提供用户提交的参数,基础数据类型和用户支持的语言列表。最后两个用于渲染FormMessages以支持i18n(稍后将详细介绍)。
- Writer monad跟踪Enctype。表单将始终为UrlEncoded,除非有文件输入字段,这将强制我们使用multipart。
- State monad跟踪生成的字段名称和标识符。
AForm非常相似。但是,有一些主要差异:
- 它生成一个FieldView列表,用于跟踪我们将向用户显示的内容。这使我们能够保持表单显示的抽象,然后在结束时选择适当的函数将其放在页面上。在概要中,我们使用了renderDivs,它创建了一堆div标签。另外两个选项是renderBootstrap和renderTable。
- 它不提供Monad实例。Applicative的目标是允许整个表单运行,尽可能多地获取每个字段的信息,然后创建最终结果。这在Monad的背景下不起作用。
FormInput甚至更简单:它返回错误消息列表或结果。
转换
"但是等一下,"你说。"你说的概要使用了applicative的表单,但我确定类型签名是MForm。不应该是Monadic吗?"这是真的,我们制作的最终表单是monadic。但真正发生的是我们将一个applicative 表单转换为一个monadic 表单。
同样,我们的目标是尽可能多的重用代码,并最大限度的减少API中的函数数量。Monadic形式比Applicative更强大,所以任何可以用Applicative表单表达的东西也可以用Monadic表单表达。有两个核心函数可以帮助解决这个问题:aformToForm
将任何applicative的表单转换为monadic表单,formToAForm将某些类型的monadic形式转换为Applicative表单。
“但是等一下,”你坚持道。 “我没有看到任何aformToForm!”的确是这样是renderDivs函数帮我们处理了。
创建AForms
现在,我(希望)说服你,在我们的概要中,我们真的在处理applicative的表单,让我们看看并尝试理解这些事情是如何创建的。我们举一个简单的例子:
data Car = Car
{ carModel :: Text
, carYear :: Int
}
deriving Show
carAForm :: AForm Handler Car
carAForm = Car
<$> areq textField "Model" Nothing
<*> areq intField "Year" Nothing
carForm :: Html -> MForm Handler (FormResult Car, Widget)
carForm = renderTable carAForm
在这里,我们明确地分开了applicative
和monadic
表单。在carAForm中,我们使用<$>和<*>运算符。这应该不足为奇;这是applicative
的常用代码。我们的Car数据类型中的每条记录都对应一行。也许不出所料,我们有Text记录的textField和Int记录的intField。 让我们更仔细地看看areq函数。它简化的类型签名是Field a -> FieldSettings -> Maybe a -> AForm a
。第一个参数指定此字段的数据类型,如何解析它以及如何呈现它。下一个参数FieldSettings告诉我们字段的标签,工具提示,名称和ID。在这种情况下,我们使用前面提到的FieldSettings的IsString实例。
那Maybe a
是什么?它提供可选的默认值。例如,如果我们希望我们的表单填写“2007”作为默认的汽车年份,我们将使用areq intField“Year”(Just 2007)
。我们甚至可以将它提升到一个新的水平,有一个表单,该表单采用可选参数给出默认值。
carAForm :: Maybe Car -> AForm Handler Car
carAForm mcar = Car
<$> areq textField "Model" (carModel <$> mcar)
<*> areq intField "Year" (carYear <$> mcar)
可选字段
假设我们想要一个可选字段(比如汽车颜色)。我们所做的只是使用aopt函数。
carAForm :: AForm Handler Car
carAForm = Car
<$> areq textField "Model" Nothing
<*> areq intField "Year" Nothing
<*> aopt textField "Color" Nothing
和必填字段一样,最后一个参数是可选的默认值。但是,这有两层Maybe。这实际上有点多余,但它使得编写采用可选的默认表单参数的代码变得更加容易,例如在下一个示例中。
carAForm :: Maybe Car -> AForm Handler Car
carAForm mcar = Car
<$> areq textField "Model" (carModel <$> mcar)
<*> areq intField "Year" (carYear <$> mcar)
<*> aopt textField "Color" (carColor <$> mcar)
carForm :: Html -> MForm Handler (FormResult Car, Widget)
carForm = renderTable $ carAForm $ Just $ Car "Forte" 2010 $ Just "gray"
验证
我们如何让我们的形式只接受1990年以后创造的汽车?如果你还记得,我们上面说过,Field本身包含了什么是有效条目的信息.所以我们需要做的就是写一个新的Field,对吗?好吧,那会有点单调乏味。相反,我们只修改一个现有的:
carAForm :: Maybe Car -> AForm Handler Car
carAForm mcar = Car
<$> areq textField "Model" (carModel <$> mcar)
<*> areq carYearField "Year" (carYear <$> mcar)
<*> aopt textField "Color" (carColor <$> mcar)
where
errorMessage :: Text
errorMessage = "Your car is too old, get a new one!"
carYearField = check validateYear intField
validateYear y
| y < 1990 = Left errorMessage
| otherwise = Right y
这里的技巧是check
函数。它使用一个返回错误消息或修改了的字段值的函数(validateYear)。在这个例子中,我们根本没有修改过这个值。通常情况就是这样。这种检查很常见,所以我们有一个简写:
carYearField = checkBool (>= 1990) errorMessage intField
checkBool有两个参数:必须满足条件,如果不满足则显示错误消息。
您可能已经注意到errorMessage上的显式文本类型签名。在OverloadedStrings存在的情况下,这是必要的。为了支持i18n,消息可以有许多不同的数据类型,GHC无法确定您打算使用哪个IsString实例。
确保汽车不会太旧是很棒的。但是,如果我们想确保指定的年份不是来自未来呢?为了查看当前年份,我们需要运行一些IO。对于这种情况,我们需要checkM,它允许我们的验证代码执行任意操作:
carYearField = checkM inPast $ checkBool (>= 1990) errorMessage intField
inPast y = do
thisYear <- liftIO getCurrentYear
return $ if y <= thisYear
then Right y
else Left ("You have a time machine!" :: Text)
getCurrentYear :: IO Int
getCurrentYear = do
now <- getCurrentTime
let today = utctDay now
let (year, _, _) = toGregorian today
return $ fromInteger year
inPast是一个函数,它将在Handler monad中返回Either结果。我们使用liftIO getCurrentYear来获取当前年份,然后将其与用户提供的年份进行比较。另外,请注意我们如何将多个验证器链接在一起。
由于checkM验证器在Handler monad中运行,因此它可以访问您在Yesod中通常可以执行的许多操作。这对于运行数据库操作特别有用,我们将在Persistent章节中介绍。
更复杂的字段
我们的颜色输入字段很好,但它并不完全是用户友好的。我们真正想要的是一个下拉列表。
data Car = Car
{ carModel :: Text
, carYear :: Int
, carColor :: Maybe Color
}
deriving Show
data Color = Red | Blue | Gray | Black
deriving (Show, Eq, Enum, Bounded)
carAForm :: Maybe Car -> AForm Handler Car
carAForm mcar = Car
<$> areq textField "Model" (carModel <$> mcar)
<*> areq carYearField "Year" (carYear <$> mcar)
<*> aopt (selectFieldList colors) "Color" (carColor <$> mcar)
where
colors :: [(Text, Color)]
colors = [("Red", Red), ("Blue", Blue), ("Gray", Gray), ("Black", Black)]
selectFieldList接收一个元组对的列表。这个元组对中的第一项是在下拉列表中向用户显示的文本,第二项是实际的Haskell值。当然,上面的代码看起来非常重复;我们可以使用Enum和Bounded实例获得相同的结果GHC自动为我们派生。
colors = map (pack . show &&& id) [minBound..maxBound]
[minBound..maxBound]
为我们提供了所有不同颜色值的列表。我们可以用map
和&&&
(Arrow里的方法)把他转化成一个元组对列表。甚至可以通过使用yesod-form提供的optionsEnum函数来简化这一过程,这会将原始代码转换为:
carAForm :: Maybe Car -> AForm Handler Car
carAForm mcar = Car
<$> areq textField "Model" (carModel <$> mcar)
<*> areq carYearField "Year" (carYear <$> mcar)
<*> aopt (selectField optionsEnum) "Color" (carColor <$> mcar)
有些人更喜欢单选按钮来下拉列表。幸运的是,这只是一个单词的改变。
carAForm = Car
<$> areq textField "Model" Nothing
<*> areq intField "Year" Nothing
<*> aopt (radioField optionsEnum) "Color" Nothing
##运行表单 我们想要获取我们的表单的一些值。有许多不同的函数可用于此,每个功能都有其自己的用途。我将学习它们,首先从最常见的开始。
runFormPost
这针对任何通过POST提交的表单。如果这不是POST提交,它将返回FormMissing
。这会自动将安全令牌作为隐藏表单字段插入,以避免跨站点请求伪造(CSRF)攻击。
runFormGet
和runFormPost差不多但是它读取GET参数。为了区分正常的GET页面查询和GET提交,它在表单中包含一个额外的_hasdata隐藏字段。与runFormPost不同,它不包括CSRF保护。
runFormPostNoToken
与runFormPost相同,但不包括(或要求)CSRF安全令牌。
generateFormPost
同绑定到现有的POST参数不同。如果要在提交上一个表单后生成新表单(例如在向导中),这可能很有用。
generateFormGet
与generateFormPost相同,但相对于GET。
前三个的返回类型是((FormResult a,Widget),Enctype)
。Widget已经存有任何验证错误和先前提交的值。
为什么嵌套元组而不是专用数据类型?这是因为runFormPostNoToken和runFormGet都可以用于不返回FormResult或Widget的表单,这在处理更复杂的monadic表单时很有用(下面讨论)。
i18n (国际化)
本章中有一些对i18n的引用。该主题将在其章节中得到更全面的介绍,但由于它对yesod-form有如此深远的影响,我想简要概述一下。在Yesod中i18n的思想是让数据类型代表消息。对于给定的数据类型,每个站点都可以有一个RenderMessage实例,该实例将根据用户接受的语言列表转换该消息。由于这一些,您应该注意以下几点:
- 每个站点都有一个RenderMessage for Text的自动实现,因此如果你不关心i18n支持,你可以使用普通字符串。但是,您可能需要偶尔使用显式类型签名。
- yesod-form以FormMessage数据类型表示其所有消息。因此,要使用yesod-form,您需要具有适当的RenderMessage实例。使用默认英语翻译的简单方法是:
instance RenderMessage App FormMessage where
renderMessage _ _ = defaultFormMessage
这是由scaffolded网站自动提供的。
Monadic 表单
通常,简单的表单布局是足够的,并且applicative表单在这种方法中表现良好。但是,有时候,您需要为表单提供更加自定义的外观。
非标准表单布局
对于这些用例,monadic形式更符合要求。他比其他的兄弟更冗长,但这种冗长让你可以完全控制表格的样子。为了生成上面的表单,我们可以编写类似这样的代码。
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
import Control.Applicative
import Data.Text (Text)
import Yesod
data App = App
mkYesod "App" [parseRoutes|
/ HomeR GET
|]
instance Yesod App
instance RenderMessage App FormMessage where
renderMessage _ _ = defaultFormMessage
data Person = Person
{ personName :: Text
, personAge :: Int
}
deriving Show
personForm :: Html -> MForm Handler (FormResult Person, Widget)
personForm extra = do
(nameRes, nameView) <- mreq textField "this is not used" Nothing
(ageRes, ageView) <- mreq intField "neither is this" Nothing
let personRes = Person <$> nameRes <*> ageRes
let widget = do
toWidget
[lucius|
##{fvId ageView} {
width: 3em;
}
|]
[whamlet|
#{extra}
<p>
Hello, my name is #
^{fvInput nameView}
\ and I am #
^{fvInput ageView}
\ years old. #
<input type=submit value="Introduce myself">
|]
return (personRes, widget)
getHomeR :: Handler Html
getHomeR = do
((res, widget), enctype) <- runFormGet personForm
defaultLayout
[whamlet|
<p>Result: #{show res}
<form enctype=#{enctype}>
^{widget}
|]
main :: IO ()
main = warp 3000 App
类似于applicative areq,我们为monadic 表单使用mreq。(是的,可选字段也有mopt。)但是有一个很大的不同:mreq给了我们一个元组值对。我们可以根据需要插入它,而不是隐藏FieldView值并自动将其插入到窗口小部件中。 FieldView有许多信息。最重要的是fvInput,它是实际的表单字段。在这个例子中,我们还使用了fvId,它返回了输入标记的HTML id属性。在我们的示例中,我们使用它来指定字段的宽度。 你可能想知道“这个没用过”和“不是这个”的值。 mreq将FieldSettings作为其第二个参数。由于FieldSettings提供了一个IsString实例,因此编译器基本上将字符串扩展为:
fromString "this is not used" == FieldSettings
{ fsLabel = "this is not used"
, fsTooltip = Nothing
, fsId = Nothing
, fsName = Nothing
, fsAttrs = []
}
对于applicative表单,在构造HTML时使用fsLabel和fsTooltip值。在monadic表单的情况下,Yesod不会为您生成任何“包装”HTML,因此会忽略这些值。但是,我们仍然保留FieldSettings参数,以允许您根据需要覆盖字段的id和name属性。
另一个有趣的是额外的值。GET表单包含一个额外的字段,表示它们已被提交,POST表单包含一个安全令牌以防止CSRF攻击。如果您未在表单中包含此额外隐藏字段,则表单提交将失败。
除此之外,事情非常简单。我们通过将nameRes和ageRes值组合在一起来创建personRes值,然后返回person和widget的元组。在getHomeR函数中,一切看起来都像一个应用形式。事实上,你可以用一个applicative替换我们的monadic形式,代码仍然可以工作。
Input forms
Applicative和monadic表单处理HTML代码的生成和用户输入的解析。有时,您只想执行后者,例如某个地方的HTML已存在的表单,或者您想使用Javascript动态生成表单。在这种情况下,您需要Input表单。
这些工作大多与应用和monadic表单相同,但有一些差异:
- 使用runInputPost和runInputGet。
- 使用ireq和iopt。这些函数现在只有两个参数:字段类型和相关字段的名称(即HTML名称属性)。
- 运行表单后,它返回值。它不返回Widget或编码类型。
- 如果存在任何验证错误,页面将返回“无效参数”错误页面。
您可以使用input表单重新创建上一个示例。但请注意,input表单版本的用户友好性较差。如果您在应用程序或monadic形式中出错,您将返回到同一页面,其中包含您之前在表单中输入的值,以及一条错误消息,说明您需要更正的内容。使用Input表单,用户只需获取错误消息。
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
import Control.Applicative
import Data.Text (Text)
import Yesod
data App = App
mkYesod "App" [parseRoutes|
/ HomeR GET
/input InputR GET
|]
instance Yesod App
instance RenderMessage App FormMessage where
renderMessage _ _ = defaultFormMessage
data Person = Person
{ personName :: Text
, personAge :: Int
}
deriving Show
getHomeR :: Handler Html
getHomeR = defaultLayout
[whamlet|
<form action=@{InputR}>
<p>
My name is
<input type=text name=name>
and I am
<input type=text name=age>
years old.
<input type=submit value="Introduce myself">
|]
getInputR :: Handler Html
getInputR = do
person <- runInputGet $ Person
<$> ireq textField "name"
<*> ireq intField "age"
defaultLayout [whamlet|<p>#{show person}|]
main :: IO ()
main = warp 3000 App
自定义字段
与Yesod一起内置的字段可能会满足您的绝大多数表单需求。但偶尔,你需要更专业的东西。幸运的是,您可以自己在Yesod中创建新字段。 Field构造函数有三个值:fieldParse获取用户提交的值列表,并返回以下三个结果之一:
- 验证失败的错误消息。
- 解析后的值。
Nothing
,表明没有提供数据。
最后一种情况可能听起来令人惊讶。看似Yesod可以自动知道输入列表为空时没有提供任何信息。但实际上,对于某些字段类型,缺少任何输入实际上都是有效的输入。例如,复选框通过发送空列表来指示未检查状态。
此外,列表怎么办?不应该是一个Maybe
吗?事实并非如此。使用分组复选框和多选列表,您将拥有多个具有相同名称的小部件。我们在下面的示例中也使用了这个技巧。
构造函数中的第二个值是fieldView,它呈现一个窗口小部件以显示给用户。该函数具有以下参数:
id
属性。name
属性。- 任何其他属性。
- 结果,返回Either值。这将提供未解析的输入(解析失败时)或成功解析的值。intField是一个很好的例子。如果输入42,结果的值将为Right 42。但如果你输入
乌龟
,结果将是Left "乌龟"
。这使您可以在输入标记上添加值属性,从而为用户提供一致的体验。 - Bool表示是否需要该字段。
构造函数中的最后值是fieldEnctype
。如果你正在处理文件上传,那应该是Multipart
;否则,它应该是UrlEncoded
。
作为一个小例子,让我们创建一个新的字段类型,它是一个密码确认字段。此字段有两个文本inputs-
两者都具有相同的名称attribute-
如果值不匹配则返回错误消息,请注意,与大多数字段不同,它不会在输入标记上提供值属性,因为您不希望在HTML中发回用户输入的密码。
passwordConfirmField :: Field Handler Text
passwordConfirmField = Field
{ fieldParse = \rawVals _fileVals ->
case rawVals of
[a, b]
| a == b -> return $ Right $ Just a
| otherwise -> return $ Left "Passwords don't match"
[] -> return $ Right Nothing
_ -> return $ Left "You must enter two values"
, fieldView = \idAttr nameAttr otherAttrs eResult isReq ->
[whamlet|
<input id=#{idAttr} name=#{nameAttr} *{otherAttrs} type=password>
<div>Confirm:
<input id=#{idAttr}-confirm name=#{nameAttr} *{otherAttrs} type=password>
|]
, fieldEnctype = UrlEncoded
}
getHomeR :: Handler Html
getHomeR = do
((res, widget), enctype) <- runFormGet $ renderDivs
$ areq passwordConfirmField "Password" Nothing
defaultLayout
[whamlet|
<p>Result: #{show res}
<form enctype=#{enctype}>
^{widget}
<input type=submit value="Change password">
|]
不是来自用户的值
想象一下,您正在撰写一个托管Web应用程序的博客,并且您希望有一个表单供用户输入博客文章。博客文章将包含四条信息:
- Title
- HTML内容。 *作者的ID *发布日期
我们希望用户输入前两个值,但不能输入后两个值。户ID应通过验证用户自动确定(我们尚未涉及的主题),发布日期应该是当前时间。问题是,我们如何保持简单的应用形式语法,然后引入不是来自用户的值?
答案是两个独立的辅助函数:
pure
允许我们将一个普通值包装成一个适用的表单值。- lift允许我们在applicative表单中运行任意Handler操作。
让我们看一个使用这两个函数的示例:
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
import Control.Applicative
import Data.Text (Text)
import Data.Time
import Yesod
-- In the authentication chapter, we'll address this properly
newtype UserId = UserId Int
deriving Show
data App = App
mkYesod "App" [parseRoutes|
/ HomeR GET POST
|]
instance Yesod App
instance RenderMessage App FormMessage where
renderMessage _ _ = defaultFormMessage
type Form a = Html -> MForm Handler (FormResult a, Widget)
data Blog = Blog
{ blogTitle :: Text
, blogContents :: Textarea
, blogUser :: UserId
, blogPosted :: UTCTime
}
deriving Show
form :: UserId -> Form Blog
form userId = renderDivs $ Blog
<$> areq textField "Title" Nothing
<*> areq textareaField "Contents" Nothing
<*> pure userId
<*> lift (liftIO getCurrentTime)
getHomeR :: Handler Html
getHomeR = do
let userId = UserId 5 -- again, see the authentication chapter
((res, widget), enctype) <- runFormPost $ form userId
defaultLayout
[whamlet|
<p>Previous result: #{show res}
<form method=post action=@{HomeR} enctype=#{enctype}>
^{widget}
<input type=submit>
|]
postHomeR :: Handler Html
postHomeR = getHomeR
main :: IO ()
main = warp 3000 App
我们在这里介绍的一个技巧是为GET和POST请求方法使用相同的处理程序代码。这是通过runFormPost的实现来实现的,在GET请求的情况下,runFormPost的行为与generateFormPost完全相同。对两种请求方法使用相同的处理程序可以减少一些样板。
结语
Yesod中的表格分为三组。 Applicative是最常见的,因为它提供了一个很好的用户界面和一个易于使用的API。 Monadic形式给你更多力量,但更难使用。当您只想从用户读取数据而不是生成输入窗口小部件时,就会使用input表单。
Yesod提供了许多不同的字段,开箱即用。为了在表单中使用这些,您需要指明表单的类型以及字段是必需的还是可选的。结果是六个辅助函数:areq,aopt,mreq,mopt,ireq和iopt。
表格具有强大的能力。他们可以自动插入Javascript以帮助您利用更好的UI控件,例如jQuery UI日期选择器。表格也完全支持i18n,因此您可以支持全球用户社区。当您有更多特定需求时,您可以将某些验证功能发送到现有字段,或者从头开始编写新功能。