Yesod - 表单 (7)

我们已经提到了边界问题:每当数据进入或离开应用程序时,我们都需要对其进行验证。可能最困难的地方就是表单。表单代码编写很复杂;在理想的情况下中,我们想要一个解决以下问题的解决方案:

  • 确保数据有效。
  • 将原始的字符串数据提交到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

在这里,我们明确地分开了applicativemonadic表单。在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,它呈现一个窗口小部件以显示给用户。该函数具有以下参数:

  1. id属性。
  2. name属性。
  3. 任何其他属性。
  4. 结果,返回Either值。这将提供未解析的输入(解析失败时)或成功解析的值。intField是一个很好的例子。如果输入42,结果的值将为Right 42。但如果你输入乌龟,结果将是 Left "乌龟"。这使您可以在输入标记上添加值属性,从而为用户提供一致的体验。
  5. 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,因此您可以支持全球用户社区。当您有更多特定需求时,您可以将某些验证功能发送到现有字段,或者从头开始编写新功能。

转载于:https://my.oschina.net/mzui/blog/1939610

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值