The widget generated contains only the contents of the form, not the form tag itself. So...
It also doesn't include the submit button. Submit |] postPersonR :: Handler Html postPersonR = do ((result, widget), enctype) <- runFormPost personForm case result of FormSuccess person -> defaultLayout [whamlet|#{show person}|] _ -> defaultLayout [whamlet| Invalid input, let's try again. ^{widget} Submit |] main :: IO () main = warp 3000 App ``` ## Kinds of Forms 在进入类型本身之前,我们首先介绍三种不同类型的表单。 ### Applicative (应用函子) 这是最常用的(它就是在概要demo中出现的那个)。Applicative为我们提供了一些很好的方法,让错误消息可以合并在一起并保持一种非常高级的声明性方法。 (有关Applicative代码的更多信息,请参阅[Haskell wiki](https://www.haskell.org/haskellwiki/Applicative_functor "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的表单,让我们看看并尝试理解这些事情是如何创建的。我们举一个简单的例子: ```Haskell 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)`。我们甚至可以将它提升到一个新的水平,有一个表单,该表单采用可选参数给出默认值。 ```Haskell carAForm :: Maybe Car -> AForm Handler Car carAForm mcar = Car <$> areq textField "Model" (carModel <$> mcar) <*> areq intField "Year" (carYear <$> mcar) ``` ## 可选字段 假设我们想要一个可选字段(比如汽车颜色)。我们所做的只是使用aopt函数。 ```Haskell carAForm :: AForm Handler Car carAForm = Car <$> areq textField "Model" Nothing <*> areq intField "Year" Nothing <*> aopt textField "Color" Nothing ``` 和必填字段一样,最后一个参数是可选的默认值。但是,这有两层Maybe。这实际上有点多余,但它使得编写采用可选的默认表单参数的代码变得更加容易,例如在下一个示例中。 ```Haskell 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,对吗?好吧,那会有点单调乏味。相反,我们只修改一个现有的: ```Haskell 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)。在这个例子中,我们根本没有修改过这个值。通常情况就是这样。这种检查很常见,所以我们有一个简写: ```Haskell carYearField = checkBool (>= 1990) errorMessage intField ``` checkBool有两个参数:必须满足条件,如果不满足则显示错误消息。 > 您可能已经注意到errorMessage上的显式文本类型签名。在OverloadedStrings存在的情况下,这是必要的。为了支持i18n,消息可以有许多不同的数据类型,GHC无法确定您打算使用哪个IsString实例。 确保汽车不会太旧是很棒的。但是,如果我们想确保指定的年份不是来自未来呢?为了查看当前年份,我们需要运行一些IO。对于这种情况,我们需要checkM,它允许我们的验证代码执行任意操作: ```Haskell 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章节中介绍。 ## 更复杂的字段 我们的颜色输入字段很好,但它并不完全是用户友好的。我们真正想要的是一个下拉列表。 ```Haskell 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自动为我们派生。 ```Haskell colors = map (pack . show &&& id) [minBound..maxBound] ``` `[minBound..maxBound]`为我们提供了所有不同颜色值的列表。我们可以用`map`和`&&&`(Arrow里的方法)把他转化成一个元组对列表。甚至可以通过使用yesod-form提供的optionsEnum函数来简化这一过程,这会将原始代码转换为: ```Haskell 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) ``` 有些人更喜欢单选按钮来下拉列表。幸运的是,这只是一个单词的改变。 ```Haskell 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实例。使用默认英语翻译的简单方法是: ```Haskell instance RenderMessage App FormMessage where renderMessage _ _ = defaultFormMessage ``` 这是由scaffolded网站自动提供的。 ## Monadic 表单 通常,简单的表单布局是足够的,并且applicative表单在这种方法中表现良好。但是,有时候,您需要为表单提供更加自定义的外观。 > 非标准表单布局 ![](https://oscimg.oschina.net/oscnet/1e68f8ce13e75eb47f2ea7f0e7df7a537f7.jpg) 对于这些用例,monadic形式更符合要求。他比其他的兄弟更冗长,但这种冗长让你可以完全控制表格的样子。为了生成上面的表单,我们可以编写类似这样的代码。 ```Haskell {-# 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} Hello, my name is # ^{fvInput nameView} \ and I am # ^{fvInput ageView} \ years old. # |] return (personRes, widget) getHomeR :: Handler Html getHomeR = do ((res, widget), enctype) <- runFormGet personForm defaultLayout [whamlet| Result: #{show res} ^{widget} |] main :: IO () main = warp 3000 App ``` 类似于applicative areq,我们为monadic 表单使用mreq。(是的,可选字段也有mopt。)但是有一个很大的不同:mreq给了我们一个元组值对。我们可以根据需要插入它,而不是隐藏FieldView值并自动将其插入到窗口小部件中。 FieldView有许多信息。最重要的是fvInput,它是实际的表单字段。在这个例子中,我们还使用了fvId,它返回了输入标记的HTML id属性。在我们的示例中,我们使用它来指定字段的宽度。 你可能想知道“这个没用过”和“不是这个”的值。 mreq将FieldSettings作为其第二个参数。由于FieldSettings提供了一个IsString实例,因此编译器基本上将字符串扩展为: ```Haskell 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表单,用户只需获取错误消息。 ```Haskell {-# 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| My name is and I am years old. |] getInputR :: Handler Html getInputR = do person <- runInputGet $ Person <$> ireq textField "name" <*> ireq intField "age" defaultLayout [whamlet|#{show person}|] main :: IO () main = warp 3000 App ``` ## 自定义字段 与Yesod一起内置的字段可能会满足您的绝大多数表单需求。但偶尔,你需要更专业的东西。幸运的是,您可以自己在Yesod中创建新字段。 Field构造函数有三个值:fieldParse获取用户提交的值列表,并返回以下三个结果之一: * 验证失败的错误消息。 * 解析后的值。 * `Nothing`,表明没有提供数据。 最后一种情况可能听起来令人惊讶。看似Yesod可以自动知道输入列表为空时没有提供任何信息。但实际上,对于某些字段类型,缺少任何输入实际上都是有效的输入。例如,复选框通过发送空列表来指示未检查状态。 此外,列表怎么办?不应该是一个`Maybe`吗?事实并非如此。使用分组复选框和多选列表,您将拥有多个具有相同名称的小部件。我们在下面的示例中也使用了这个技巧。 构造函数中的第二个值是fieldView,它呈现一个窗口小部件以显示给用户。该函数具有以下参数: 1. `id`属性。 2. `name`属性。 3. 任何其他属性。 4. 结果,返回Either值。这将提供未解析的输入(解析失败时)或成功解析的值。intField是一个很好的例子。如果输入42,结果的值将为Right 42。但如果你输入`乌龟`,结果将是 `Left "乌龟"`。这使您可以在输入标记上添加值属性,从而为用户提供一致的体验。 5. Bool表示是否需要该字段。 构造函数中的最后值是`fieldEnctype`。如果你正在处理文件上传,那应该是`Multipart`;否则,它应该是`UrlEncoded`。 作为一个小例子,让我们创建一个新的字段类型,它是一个密码确认字段。此字段有两个文本` inputs- ` 两者都具有相同的名称`attribute- ` 如果值不匹配则返回错误消息,请注意,与大多数字段不同,它不会在输入标记上提供值属性,因为您不希望在HTML中发回用户输入的密码。 ```Haskell 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| Confirm: |] , fieldEnctype = UrlEncoded } getHomeR :: Handler Html getHomeR = do ((res, widget), enctype) <- runFormGet $ renderDivs $ areq passwordConfirmField "Password" Nothing defaultLayout [whamlet| Result: #{show res} ^{widget} |] ``` ## 不是来自用户的值 想象一下,您正在撰写一个托管Web应用程序的博客,并且您希望有一个表单供用户输入博客文章。博客文章将包含四条信息: * Title * HTML内容。 *作者的ID *发布日期 我们希望用户输入前两个值,但不能输入后两个值。户ID应通过验证用户自动确定(我们尚未涉及的主题),发布日期应该是当前时间。问题是,我们如何保持简单的应用形式语法,然后引入不是来自用户的值? 答案是两个独立的辅助函数: * `pure`允许我们将一个普通值包装成一个适用的表单值。 * lift允许我们在applicative表单中运行任意Handler操作。 让我们看一个使用这两个函数的示例: ```Haskell {-# 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| Previous result: #{show res} ^{widget} |] 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,因此您可以支持全球用户社区。当您有更多特定需求时,您可以将某些验证功能发送到现有字段,或者从头开始编写新功能。
#{show person}|] _ -> defaultLayout [whamlet|
Invalid input, let's try again.
Hello, my name is # ^{fvInput nameView} \ and I am # ^{fvInput ageView} \ years old. # |] return (personRes, widget) getHomeR :: Handler Html getHomeR = do ((res, widget), enctype) <- runFormGet personForm defaultLayout [whamlet|
Result: #{show res}
My name is and I am years old. |] getInputR :: Handler Html getInputR = do person <- runInputGet $ Person <$> ireq textField "name" <*> ireq intField "age" defaultLayout [whamlet|
#{show person}|] main :: IO () main = warp 3000 App ``` ## 自定义字段 与Yesod一起内置的字段可能会满足您的绝大多数表单需求。但偶尔,你需要更专业的东西。幸运的是,您可以自己在Yesod中创建新字段。 Field构造函数有三个值:fieldParse获取用户提交的值列表,并返回以下三个结果之一: * 验证失败的错误消息。 * 解析后的值。 * `Nothing`,表明没有提供数据。 最后一种情况可能听起来令人惊讶。看似Yesod可以自动知道输入列表为空时没有提供任何信息。但实际上,对于某些字段类型,缺少任何输入实际上都是有效的输入。例如,复选框通过发送空列表来指示未检查状态。 此外,列表怎么办?不应该是一个`Maybe`吗?事实并非如此。使用分组复选框和多选列表,您将拥有多个具有相同名称的小部件。我们在下面的示例中也使用了这个技巧。 构造函数中的第二个值是fieldView,它呈现一个窗口小部件以显示给用户。该函数具有以下参数: 1. `id`属性。 2. `name`属性。 3. 任何其他属性。 4. 结果,返回Either值。这将提供未解析的输入(解析失败时)或成功解析的值。intField是一个很好的例子。如果输入42,结果的值将为Right 42。但如果你输入`乌龟`,结果将是 `Left "乌龟"`。这使您可以在输入标记上添加值属性,从而为用户提供一致的体验。 5. Bool表示是否需要该字段。 构造函数中的最后值是`fieldEnctype`。如果你正在处理文件上传,那应该是`Multipart`;否则,它应该是`UrlEncoded`。 作为一个小例子,让我们创建一个新的字段类型,它是一个密码确认字段。此字段有两个文本` inputs- ` 两者都具有相同的名称`attribute- ` 如果值不匹配则返回错误消息,请注意,与大多数字段不同,它不会在输入标记上提供值属性,因为您不希望在HTML中发回用户输入的密码。 ```Haskell 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|
Previous result: #{show res}