yesod-form 1.3.15.1 → 1.7.9.3
raw patch · 26 files changed
Files
- ChangeLog.md +161/−0
- README.md +15/−0
- Yesod/Form/Bootstrap3.hs +87/−38
- Yesod/Form/Fields.hs +437/−100
- Yesod/Form/Functions.hs +203/−28
- Yesod/Form/I18n/Chinese.hs +28/−0
- Yesod/Form/I18n/Czech.hs +3/−1
- Yesod/Form/I18n/Dutch.hs +28/−0
- Yesod/Form/I18n/English.hs +3/−1
- Yesod/Form/I18n/French.hs +3/−1
- Yesod/Form/I18n/German.hs +3/−1
- Yesod/Form/I18n/Japanese.hs +3/−1
- Yesod/Form/I18n/Korean.hs +28/−0
- Yesod/Form/I18n/Norwegian.hs +3/−1
- Yesod/Form/I18n/Portuguese.hs +3/−1
- Yesod/Form/I18n/Romanian.hs +31/−0
- Yesod/Form/I18n/Russian.hs +4/−2
- Yesod/Form/I18n/Spanish.hs +28/−0
- Yesod/Form/I18n/Swedish.hs +3/−1
- Yesod/Form/Input.hs +37/−18
- Yesod/Form/Jquery.hs +20/−10
- Yesod/Form/MassInput.hs +23/−13
- Yesod/Form/Nic.hs +13/−6
- Yesod/Form/Option.hs +96/−0
- Yesod/Form/Types.hs +88/−13
- yesod-form.cabal +30/−26
+ ChangeLog.md view
@@ -0,0 +1,161 @@+# ChangeLog for yesod-form++## 1.7.9.3++* Support `yesod-core` 1.7++## 1.7.9.2++* Improve deprecation messages for `radioField` and `checkboxesField` [#1902](https://github.com/yesodweb/yesod/pull/1902)++## 1.7.9.1++* Set `base >= 4.11` for less CPP and imports [#1876](https://github.com/yesodweb/yesod/pull/1876)++## 1.7.9++* Added `checkboxesField'` for creating checkbox in more correct way than original `checkboxesField`+ Function `checkboxesField` marked as deprecated. [#1843](https://github.com/yesodweb/yesod/pull/1843)++## 1.7.8++* Added `radioField'` for creating radio button in more correct way than original `radioField`.+ Function `radioField` marked as deprecated. [#1842](https://github.com/yesodweb/yesod/pull/1842)++## 1.7.7++* Added `optionsFromList'` to create an OptionList from a List, using the PathPiece instance for the external value and+a custom function for the user-facing value. Also added `optionsEnum'` to create an OptionList from an enumeration+[#1828](https://github.com/yesodweb/yesod/pull/1828)++## 1.7.6++* Added `datetimeLocalField` for creating a html `<input type="datetime-local">` [#1817](https://github.com/yesodweb/yesod/pull/1817)++## 1.7.5++* Add Romanian translation [#1801](https://github.com/yesodweb/yesod/pull/1801)++## 1.7.4++* Added a `Monad AForm` instance only when `transformers` >= 0.6 [#1795](https://github.com/yesodweb/yesod/pull/1795)++## 1.7.3++* Fixed `radioField` according to Bootstrap 3 docs. [#1783](https://github.com/yesodweb/yesod/pull/1783)++## 1.7.2++* Added `withRadioField` and re-express `radioField` into that. [#1775](https://github.com/yesodweb/yesod/pull/1775)++## 1.7.1++* Added `colorField` for creating a html color field (`<input type="color">`) [#1748](https://github.com/yesodweb/yesod/pull/1748)++## 1.7.0++* Extended `OptionList` by `OptionListGrouped` and implemented grouped select fields (`<select>` with `<optgroup>`) [#1722](https://github.com/yesodweb/yesod/pull/1722)++## 1.6.7++* Added equivalent version of `mreqMsg` for `areq` and `wreq` correspondingly [#1628](https://github.com/yesodweb/yesod/pull/1628)++## 1.6.6++* Added `mreqMsg` for `mreq` functionality with a configurable MsgValueRequired [#1613](https://github.com/yesodweb/yesod/pull/1613)++## 1.6.5++* Add `.sr-only` to labels in `renderBootstrap3` when they are null.++## 1.6.4++* Make FormResult an instance of Eq++## 1.6.3++* make sure a select field does not lose the selected value even if a validation on the+ field fails++## 1.6.2++* Move `addClass` from private/undocumented in `Yesod.Form.Bootstrap3` to `Yesod.Form.Functions` [#1510](https://github.com/yesodweb/yesod/pull/1510)+* Add `Yesod.Form.Functions.removeClass` [#1510](https://github.com/yesodweb/yesod/pull/1510)+* Changed `Textarea` to derive `IsString` [#1514](https://github.com/yesodweb/yesod/pull/1514)+* Expose `selectFieldHelper` [#1530](https://github.com/yesodweb/yesod/pull/1530)++## 1.6.1++* Explicitly define `(<>)` in the `Semigroup` instance for `Enctype`++## 1.6.0++* Upgrade to yesod-core 1.6.0++## 1.4.16++* Korean translation++## 1.4.15++* Added `Alternative` instance to `FormResult` to simplify handling pages with multiple forms.++## 1.4.14++* Added `WForm` to reduce the verbosity using monadic forms.+* Added `wreq` and `wopt` correspondent functions for `WForm`.++## 1.4.13++* Fixed `textareaField` `writeHtmlEscapedChar` trim "\r"++## 1.4.12++* Password field does not remember its previous value++## 1.4.11++* Fix warnings+* Fixed spelling errors and wording for `Yesod.Form.Functions.convertField`'s+ documentation++## 1.4.10++* Fixed `identifyForm` to properly return `FormMissing` for empty forms. [#1072](https://github.com/yesodweb/yesod/issues/1072)++## 1.4.9++* Added a `ToValue` instance for `Enctype` [#1296](https://github.com/yesodweb/yesod/pull/1296)++## 1.4.8++* Added Yesod.Form.I18n.Spanish++## 1.4.6++* Functor instances for Option/OptionList++## 1.4.5++* Foldable/Traversable instances for FormResult [#1089](https://github.com/yesodweb/yesod/pull/1089)++## 1.4.4.1++* runFormPost has wrong behavior for empty forms [#950](https://github.com/yesodweb/yesod/issues/950)++## 1.4.4++* Add a `Semigroup` instance++## 1.4.3++Added `jqueryDatePickerDayField`.++## 1.4.2.1++Documentation updates++## 1.4.2++Added `timeFieldTypeTime` and `timeFieldTypeText`, and deprecated `timeField`+itself.
+ README.md view
@@ -0,0 +1,15 @@+## yesod-form++Form handling for Yesod, in the same style as formlets. See [the forms+chapter](http://www.yesodweb.com/book/forms) of the Yesod book.++This package provides a set of basic form inputs such as text, number, time,+checkbox, select, textarea, and etc. via `Yesod.Form.Fields` module. Also,+there is `Yesod.Form.Nic` module providing richtext field using Nic editor.+However, this module is grandfathered now and Nic editor is not actively+maintained since June 2012. You can find additional richtext editor fields in+[`yesod-form-richtext`][yesod-form-richtext] package (currently in provides+support of [Summernote][summernote] editor only).++[yesod-form-richtext]:http://hackage.haskell.org/package/yesod-form-richtext+[summernote]:http://summernote.org/
Yesod/Form/Bootstrap3.hs view
@@ -1,13 +1,24 @@+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE OverloadedStrings #-}--- | Helper functions for creating forms when using Bootstrap v3.+{-# LANGUAGE TypeOperators #-}++-- | Helper functions for creating forms when using <http://getbootstrap.com/ Bootstrap 3>.+--+ module Yesod.Form.Bootstrap3- ( -- * Rendering forms+ ( -- * Example: Rendering a basic form+ -- $example++ -- * Example: Rendering a horizontal form+ -- $example2++ -- * Rendering forms renderBootstrap3 , BootstrapFormLayout(..) , BootstrapGridOptions(..) -- * Field settings+ -- $fieldSettings , bfs , withPlaceholder , withAutofocus@@ -23,14 +34,12 @@ import Control.Monad (liftM) import Data.Text (Text) import Data.String (IsString(..))+import qualified Text.Blaze.Internal as Blaze import Yesod.Core--import qualified Data.Text as T- import Yesod.Form.Types import Yesod.Form.Functions --- | Create a new 'FieldSettings' with the classes that are+-- | Create a new 'FieldSettings' with the @form-control@ class that is -- required by Bootstrap v3. -- -- Since: yesod-form 1.3.8@@ -73,12 +82,6 @@ where newAttrs = addClass "input-sm" (fsAttrs fs) -addClass :: Text -> [(Text, Text)] -> [(Text, Text)]-addClass klass [] = [("class", klass)]-addClass klass (("class", old):rest) = ("class", T.concat [old, " ", klass]) : rest-addClass klass (other :rest) = other : addClass klass rest-- -- | How many bootstrap grid columns should be taken (see -- 'BootstrapFormLayout'). --@@ -111,10 +114,10 @@ toOffset (ColLg columns) = "col-lg-offset-" ++ show columns addGO :: BootstrapGridOptions -> BootstrapGridOptions -> BootstrapGridOptions-addGO (ColXs a) (ColXs b) = ColXs (a+b)-addGO (ColSm a) (ColSm b) = ColSm (a+b)-addGO (ColMd a) (ColMd b) = ColMd (a+b)-addGO (ColLg a) (ColLg b) = ColLg (a+b)+addGO (ColXs a) (ColXs b) = ColXs (a + b)+addGO (ColSm a) (ColSm b) = ColSm (a + b)+addGO (ColMd a) (ColMd b) = ColMd (a + b)+addGO (ColLg a) (ColLg b) = ColLg (a + b) addGO a b | a > b = addGO b a addGO (ColXs a) other = addGO (ColSm a) other addGO (ColSm a) other = addGO (ColMd a) other@@ -126,25 +129,19 @@ -- -- Since: yesod-form 1.3.8 data BootstrapFormLayout =- BootstrapBasicForm- | BootstrapInlineForm+ BootstrapBasicForm -- ^ A form with labels and inputs listed vertically. See <http://getbootstrap.com/css/#forms-example>+ | BootstrapInlineForm -- ^ A form whose @\<inputs>@ are laid out horizontally (displayed as @inline-block@). For this layout, @\<label>@s are still added to the HTML, but are hidden from display. When using this layout, you must add the @form-inline@ class to your form tag. See <http://getbootstrap.com/css/#forms-inline> | BootstrapHorizontalForm- { bflLabelOffset :: !BootstrapGridOptions- , bflLabelSize :: !BootstrapGridOptions- , bflInputOffset :: !BootstrapGridOptions- , bflInputSize :: !BootstrapGridOptions- }+ { bflLabelOffset :: !BootstrapGridOptions -- ^ The left <http://getbootstrap.com/css/#grid-offsetting offset> of the @\<label>@.+ , bflLabelSize :: !BootstrapGridOptions -- ^ The number of grid columns the @\<label>@ should use.+ , bflInputOffset :: !BootstrapGridOptions -- ^ The left <http://getbootstrap.com/css/#grid-offsetting offset> of the @\<input>@ from its @\<label>@.+ , bflInputSize :: !BootstrapGridOptions -- ^ The number of grid columns the @\<input>@ should use.+ } -- ^ A form laid out using the Bootstrap grid, with labels in the left column and inputs on the right. When using this layout, you must add the @form-horizontal@ class to your form tag. Bootstrap requires additional markup for the submit button for horizontal forms; you can use 'bootstrapSubmit' in your form or write the markup manually. See <http://getbootstrap.com/css/#forms-horizontal> deriving (Show) -- | Render the given form using Bootstrap v3 conventions. ----- Sample Hamlet for 'BootstrapHorizontalForm':------ > <form .form-horizontal role=form method=post action=@{ActionR} enctype=#{formEnctype}>--- > ^{formWidget}--- > ^{bootstrapSubmit MsgSubmit}--- -- Since: yesod-form 1.3.8 renderBootstrap3 :: Monad m => BootstrapFormLayout -> FormRender m a renderBootstrap3 formLayout aform fragment = do@@ -160,7 +157,7 @@ $case formLayout $of BootstrapBasicForm $if fvId view /= bootstrapSubmitId- <label for=#{fvId view}>#{fvLabel view}+ <label :Blaze.null (fvLabel view):.sr-only for=#{fvId view}>#{fvLabel view} ^{fvInput view} ^{helpWidget view} $of BootstrapInlineForm@@ -170,7 +167,7 @@ ^{helpWidget view} $of BootstrapHorizontalForm labelOffset labelSize inputOffset inputSize $if fvId view /= bootstrapSubmitId- <label .control-label .#{toOffset labelOffset} .#{toColumn labelSize} for=#{fvId view}>#{fvLabel view}+ <label :Blaze.null (fvLabel view):.sr-only .control-label .#{toOffset labelOffset} .#{toColumn labelSize} for=#{fvId view}>#{fvLabel view} <div .#{toOffset inputOffset} .#{toColumn inputSize}> ^{fvInput view} ^{helpWidget view}@@ -183,12 +180,12 @@ -- | (Internal) Render a help widget for tooltips and errors.-helpWidget :: FieldView site -> WidgetT site IO ()+helpWidget :: FieldView site -> WidgetFor site () helpWidget view = [whamlet| $maybe tt <- fvTooltip view <span .help-block>#{tt} $maybe err <- fvErrors view- <span .help-block>#{err}+ <span .help-block .error-block>#{err} |] @@ -200,9 +197,9 @@ { bsValue :: msg -- ^ The text of the submit button. , bsClasses :: Text- -- ^ Classes added to the @<button>@.+ -- ^ Classes added to the @\<button>@. , bsAttrs :: [(Text, Text)]- -- ^ Attributes added to the @<button>@.+ -- ^ Attributes added to the @\<button>@. } deriving (Show) instance IsString msg => IsString (BootstrapSubmit msg) where@@ -219,9 +216,9 @@ -- -- > Person <$> areq textField "Name" Nothing -- > <*> areq textField "Surname" Nothing--- > <* bootstrapSubmit "Register"+-- > <* bootstrapSubmit ("Register" :: BootstrapSubmit Text) ----- (Note that @<*@ is not a typo.)+-- (Note that '<*' is not a typo.) -- -- Alternatively, you may also just create the submit button -- manually as well in order to have more control over its@@ -260,3 +257,55 @@ -- 'bootstrapSubmit' outside 'renderBootstrap3'. bootstrapSubmitId :: Text bootstrapSubmitId = "b:ootstrap___unique__:::::::::::::::::submit-id"++-- $example+-- @\<input\>@ tags in Bootstrap 3 require the @form-control@ class,+-- and so they need modified 'FieldSettings' to display correctly.+--+-- When creating your forms, use the 'bfs' function to add this class:+--+-- > personForm :: AForm Handler Person+-- > personForm = Person+-- > <$> areq textField (bfs ("Name" :: Text)) Nothing+-- > <*> areq textField (bfs ("Surname" :: Text)) Nothing+--+-- That form can then be rendered into a widget using the 'renderBootstrap3' function. Here, the form is laid out vertically using 'BootstrapBasicForm':+--+-- > (formWidget, formEnctype) <- generateFormPost $ renderBootstrap3 BootstrapBasicForm personForm+--+-- And then used in Hamlet:+--+-- > <form role=form method=post action=@{ActionR} enctype=#{formEnctype}>+-- > ^{formWidget}+-- > <button type="submit" .btn .btn-default>Submit++-- $example2+-- Yesod.Form.Bootstrap3 also supports <http://getbootstrap.com/css/#forms-horizontal horizontal, grid based forms>.+-- These forms require additional markup for the submit tag, which is provided by the 'bootstrapSubmit' function:+--+-- > personForm :: AForm Handler Person+-- > personForm = Person+-- > <$> areq textField MsgName Nothing+-- > <*> areq textField MsgSurname Nothing+-- > <* bootstrapSubmit (BootstrapSubmit MsgSubmit "btn-default" [("attribute-name","attribute-value")])+-- > -- Note: bootstrapSubmit works with all BootstrapFormLayouts, but provides the additional markup required for Bootstrap's horizontal forms.+--+-- That form can be rendered with specific grid spacing:+--+-- > (formWidget, formEnctype) <- generateFormPost $ renderBootstrap3 (BootstrapHorizontalForm (ColSm 0) (ColSm 4) (ColSm 0) (ColSm 6)) personForm+--+-- And then used in Hamlet. Note the additional @form-horizontal@ class on the form, and that a manual submit tag isn't required:+--+-- > <form .form-horizontal role=form method=post action=@{ActionR} enctype=#{formEnctype}>+-- > ^{formWidget}++-- $fieldSettings+-- This module comes with several methods to help customize your Bootstrap 3 @\<input\>@s.+-- These functions can be chained together to apply several properties to an input:+--+-- > userForm :: AForm Handler UserForm+-- > userForm = UserForm+-- > <$> areq textField nameSettings Nothing+-- > where nameSettings = withAutofocus $+-- > withPlaceholder "First name" $+-- > (bfs ("Name" :: Text))
Yesod/Form/Fields.hs view
@@ -1,9 +1,18 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE CPP #-}+{-# LANGUAGE TypeOperators #-}++-- | Field functions allow you to easily create and validate forms, cleanly handling the uncertainty of parsing user input.+--+-- When possible, the field functions use a specific input type (e.g. "number"), allowing supporting browsers to validate the input before form submission. Browsers can also improve usability with this information; for example, mobile browsers might present a specialized keyboard for an input of type "email" or "number".+--+-- See the Yesod book <http://www.yesodweb.com/book/forms chapter on forms> for a broader overview of forms in Yesod. module Yesod.Form.Fields ( -- * i18n FormMessage (..)@@ -16,6 +25,8 @@ , intField , dayField , timeField+ , timeFieldTypeTime+ , timeFieldTypeText , htmlField , emailField , multiEmailField@@ -33,73 +44,93 @@ , fileAFormReq , fileAFormOpt -- * Options+ -- $optionsOverview+ , selectFieldHelper , selectField , selectFieldList+ , selectFieldListGrouped , radioField+ , radioField' , radioFieldList- , checkboxesFieldList+ , withRadioField , checkboxesField+ , checkboxesField'+ , checkboxesFieldList , multiSelectField , multiSelectFieldList , Option (..) , OptionList (..) , mkOptionList+ , mkOptionListGrouped , optionsPersist , optionsPersistKey , optionsPairs+ , optionsPairsGrouped , optionsEnum+ , colorField+ , datetimeLocalField ) where import Yesod.Form.Types import Yesod.Form.I18n.English import Yesod.Form.Functions (parseHelper) import Yesod.Core-import Text.Hamlet import Text.Blaze (ToMarkup (toMarkup), unsafeByteString) #define ToHtml ToMarkup #define toHtml toMarkup #define preEscapedText preEscapedToMarkup-import Text.Cassius-import Data.Time (Day, TimeOfDay(..))+import Data.Time (Day, TimeOfDay(..), LocalTime (LocalTime)) import qualified Text.Email.Validate as Email import Data.Text.Encoding (encodeUtf8, decodeUtf8With) import Data.Text.Encoding.Error (lenientDecode) import Network.URI (parseURI) import Database.Persist.Sql (PersistField, PersistFieldSql (..))-import Database.Persist (Entity (..), SqlType (SqlString))+#if MIN_VERSION_persistent(2,5,0)+import Database.Persist (Entity (..), SqlType (SqlString), PersistRecordBackend, PersistQueryRead)+#else+import Database.Persist (Entity (..), SqlType (SqlString), PersistEntity, PersistQuery, PersistEntityBackend)+#endif import Text.HTML.SanitizeXSS (sanitizeBalance)-import Control.Monad (when, unless)+import Control.Monad (when, unless, forM_) import Data.Either (partitionEithers) import Data.Maybe (listToMaybe, fromMaybe) import qualified Blaze.ByteString.Builder.Html.Utf8 as B import Blaze.ByteString.Builder (writeByteString, toLazyByteString) import Blaze.ByteString.Builder.Internal.Write (fromWriteList)-import Database.Persist (PersistMonadBackend, PersistEntityBackend) import Text.Blaze.Html.Renderer.String (renderHtml) import qualified Data.ByteString as S import qualified Data.ByteString.Lazy as L import Data.Text as T ( Text, append, concat, cons, head- , intercalate, isPrefixOf, null, unpack, pack, splitOn+ , intercalate, isPrefixOf, null, unpack, pack+ , split, splitOn )-import qualified Data.Text as T (drop, dropWhile) +import qualified Data.Text as T (drop, dropWhile) import qualified Data.Text.Read import qualified Data.Map as Map-import Yesod.Persist (selectList, runDB, Filter, SelectOpt, Key, YesodPersist, PersistEntity, PersistQuery)+import Yesod.Persist (selectList, Filter, SelectOpt, Key) import Control.Arrow ((&&&)) -import Control.Applicative ((<$>), (<|>))+import Control.Applicative ((<|>)) import Data.Attoparsec.Text (Parser, char, string, digit, skipSpace, endOfInput, parseOnly) import Yesod.Persist.Core +import Data.String (IsString)+++import Data.Char (isHexDigit)++{-# DEPRECATED radioField "This function seems to have a bug (label could not be found with byLabel algorithm). Use radioField' instead." #-}+{-# DEPRECATED checkboxesField "This function seems to have a bug (label could not be found with byLabel algorithm). Use checkboxesField' instead." #-}+ defaultFormMessage :: FormMessage -> Text defaultFormMessage = englishFormMessage -+-- | Creates a input with @type="number"@ and @step=1@. intField :: (Monad m, Integral i, RenderMessage (HandlerSite m) FormMessage) => Field m i intField = Field { fieldParse = parseHelper $ \s ->@@ -117,6 +148,7 @@ showVal = either id (pack . showI) showI x = show (fromIntegral x :: Integer) +-- | Creates a input with @type="number"@ and @step=any@. doubleField :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m Double doubleField = Field { fieldParse = parseHelper $ \s ->@@ -132,6 +164,9 @@ } where showVal = either id (pack . show) +-- | Creates an input with @type="date"@, validating the input using the 'parseDate' function.+--+-- Add the @time@ package and import the "Data.Time.Calendar" module to use this function. dayField :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m Day dayField = Field { fieldParse = parseHelper $ parseDate . unpack@@ -143,12 +178,34 @@ } where showVal = either id (pack . show) +-- | An alias for 'timeFieldTypeTime'. timeField :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m TimeOfDay-timeField = Field+timeField = timeFieldTypeTime++-- | Creates an input with @type="time"@. <http://caniuse.com/#search=time%20input%20type Browsers not supporting this type> will fallback to a text field, and Yesod will parse the time as described in 'timeFieldTypeText'.+--+-- Add the @time@ package and import the "Data.Time.LocalTime" module to use this function.+--+-- @since 1.4.2+timeFieldTypeTime :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m TimeOfDay+timeFieldTypeTime = timeFieldOfType "time"++-- | Creates an input with @type="text"@, parsing the time from an [H]H:MM[:SS] format, with an optional AM or PM (if not given, AM is assumed for compatibility with the 24 hour clock system).+--+-- This function exists for backwards compatibility with the old implementation of 'timeField', which used to use @type="text"@. Consider using 'timeField' or 'timeFieldTypeTime' for improved UX and validation from the browser.+--+-- Add the @time@ package and import the "Data.Time.LocalTime" module to use this function.+--+-- @since 1.4.2+timeFieldTypeText :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m TimeOfDay+timeFieldTypeText = timeFieldOfType "text"++timeFieldOfType :: Monad m => RenderMessage (HandlerSite m) FormMessage => Text -> Field m TimeOfDay+timeFieldOfType inputType = Field { fieldParse = parseHelper parseTime , fieldView = \theId name attrs val isReq -> toWidget [hamlet| $newline never-<input id="#{theId}" name="#{name}" *{attrs} :isReq:required="" value="#{showVal val}">+<input id="#{theId}" name="#{name}" *{attrs} type="#{inputType}" :isReq:required="" value="#{showVal val}"> |] , fieldEnctype = UrlEncoded }@@ -159,21 +216,25 @@ where fullSec = fromInteger $ floor $ todSec tod +-- | Creates a @\<textarea>@ tag whose input is sanitized to prevent XSS attacks and is validated for having balanced tags. htmlField :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m Html htmlField = Field { fieldParse = parseHelper $ Right . preEscapedText . sanitizeBalance- , fieldView = \theId name attrs val _isReq -> toWidget [hamlet|+ , fieldView = \theId name attrs val isReq -> toWidget [hamlet| $newline never-<textarea id="#{theId}" name="#{name}" *{attrs}>#{showVal val}+<textarea :isReq:required="" id="#{theId}" name="#{name}" *{attrs}>#{showVal val} |] , fieldEnctype = UrlEncoded } where showVal = either id (pack . renderHtml) --- | A newtype wrapper around a 'Text' that converts newlines to HTML--- br-tags.+-- | A newtype wrapper around a 'Text' whose 'ToMarkup' instance converts newlines to HTML @\<br>@ tags.+--+-- (When text is entered into a @\<textarea>@, newline characters are used to separate lines.+-- If this text is then placed verbatim into HTML, the lines won't be separated, thus the need for replacing with @\<br>@ tags).+-- If you don't need this functionality, simply use 'unTextarea' to access the raw text. newtype Textarea = Textarea { unTextarea :: Text }- deriving (Show, Read, Eq, PersistField, Ord, ToJSON, FromJSON)+ deriving (Show, Read, Eq, PersistField, Ord, ToJSON, FromJSON, IsString) instance PersistFieldSql Textarea where sqlType _ = SqlString instance ToHtml Textarea where@@ -187,19 +248,22 @@ . unTextarea where -- Taken from blaze-builder and modified with newline handling.+ writeHtmlEscapedChar '\r' = mempty writeHtmlEscapedChar '\n' = writeByteString "<br>" writeHtmlEscapedChar c = B.writeHtmlEscapedChar c +-- | Creates a @\<textarea>@ tag whose returned value is wrapped in a 'Textarea'; see 'Textarea' for details. textareaField :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m Textarea textareaField = Field { fieldParse = parseHelper $ Right . Textarea- , fieldView = \theId name attrs val _isReq -> toWidget [hamlet|+ , fieldView = \theId name attrs val isReq -> toWidget [hamlet| $newline never-<textarea id="#{theId}" name="#{name}" *{attrs}>#{either id unTextarea val}+<textarea id="#{theId}" name="#{name}" :isReq:required="" *{attrs}>#{either id unTextarea val} |] , fieldEnctype = UrlEncoded } +-- | Creates an input with @type="hidden"@; you can use this to store information in a form that users shouldn't see (for example, Yesod stores CSRF tokens in a hidden field). hiddenField :: (Monad m, PathPiece p, RenderMessage (HandlerSite m) FormMessage) => Field m p hiddenField = Field@@ -211,6 +275,7 @@ , fieldEnctype = UrlEncoded } +-- | Creates a input with @type="text"@. textField :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m Text textField = Field { fieldParse = parseHelper $ Right@@ -221,13 +286,13 @@ |] , fieldEnctype = UrlEncoded }-+-- | Creates an input with @type="password"@. passwordField :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m Text passwordField = Field { fieldParse = parseHelper $ Right- , fieldView = \theId name attrs val isReq -> toWidget [hamlet|+ , fieldView = \theId name attrs _ isReq -> toWidget [hamlet| $newline never-<input id="#{theId}" name="#{name}" *{attrs} type="password" :isReq:required="" value="#{either id id val}">+<input id="#{theId}" name="#{name}" *{attrs} type="password" :isReq:required=""> |] , fieldEnctype = UrlEncoded }@@ -237,6 +302,7 @@ (x, _):_ -> Just x [] -> Nothing +-- | Parses a 'Day' from a 'String'. parseDate :: String -> Either FormMessage Day parseDate = maybe (Left MsgInvalidDay) Right . readMay . replace '/' '-'@@ -293,6 +359,7 @@ then fail $ show $ msg $ pack xy else return $ fromIntegral (i :: Int) +-- | Creates an input with @type="email"@. Yesod will validate the email's correctness according to RFC5322 and canonicalize it by removing comments and whitespace (see "Text.Email.Validate"). emailField :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m Text emailField = Field { fieldParse = parseHelper $@@ -307,9 +374,9 @@ , fieldEnctype = UrlEncoded } --- |+-- | Creates an input with @type="email"@ with the <http://w3c.github.io/html/sec-forms.html#the-multiple-attribute multiple> attribute; browsers might implement this as taking a comma separated list of emails. Each email address is validated as described in 'emailField'. ----- Since 1.3.7+-- @since 1.3.7 multiEmailField :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m [Text] multiEmailField = Field { fieldParse = parseHelper $@@ -333,6 +400,7 @@ emailToText = decodeUtf8With lenientDecode . Email.toByteString type AutoFocus = Bool+-- | Creates an input with @type="search"@. For <http://caniuse.com/#search=autofocus browsers without autofocus support>, a JS fallback is used if @AutoFocus@ is true. searchField :: Monad m => RenderMessage (HandlerSite m) FormMessage => AutoFocus -> Field m Text searchField autoFocus = Field { fieldParse = parseHelper Right@@ -353,7 +421,7 @@ |] , fieldEnctype = UrlEncoded }-+-- | Creates an input with @type="url"@, validating the URL according to RFC3986. urlField :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m Text urlField = Field { fieldParse = parseHelper $ \s ->@@ -365,14 +433,28 @@ , fieldEnctype = UrlEncoded } +-- | Creates a @\<select>@ tag for selecting one option. Example usage:+--+-- > areq (selectFieldList [("Value 1" :: Text, "value1"),("Value 2", "value2")]) "Which value?" Nothing selectFieldList :: (Eq a, RenderMessage site FormMessage, RenderMessage site msg) => [(msg, a)]- -> Field (HandlerT site IO) a+ -> Field (HandlerFor site) a selectFieldList = selectField . optionsPairs +-- | Creates a @\<select>@ tag with @\<optgroup>@s for selecting one option.+--+-- @since 1.7.0+selectFieldListGrouped :: (Eq a, RenderMessage site FormMessage, RenderMessage site msg)+ => [(msg, [(msg, a)])]+ -> Field (HandlerFor site) a+selectFieldListGrouped = selectField . optionsPairsGrouped++-- | Creates a @\<select>@ tag with optional @\<optgroup>@s for selecting one option. Example usage:+--+-- > areq (selectField $ optionsPairs [(MsgValue1, "value1"),(MsgValue2, "value2")]) "Which value?" Nothing selectField :: (Eq a, RenderMessage site FormMessage)- => HandlerT site IO (OptionList a)- -> Field (HandlerT site IO) a+ => HandlerFor site (OptionList a)+ -> Field (HandlerFor site) a selectField = selectFieldHelper (\theId name attrs inside -> [whamlet| $newline never@@ -386,15 +468,20 @@ $newline never <option value=#{value} :isSel:selected>#{text} |]) -- inside+ (Just $ \label -> [whamlet|+<optgroup label=#{label}>+|]) -- group label -multiSelectFieldList :: (Eq a, RenderMessage site FormMessage, RenderMessage site msg)+-- | Creates a @\<select>@ tag for selecting multiple options.+multiSelectFieldList :: (Eq a, RenderMessage site msg) => [(msg, a)]- -> Field (HandlerT site IO) [a]+ -> Field (HandlerFor site) [a] multiSelectFieldList = multiSelectField . optionsPairs -multiSelectField :: (Eq a, RenderMessage site FormMessage)- => HandlerT site IO (OptionList a)- -> Field (HandlerT site IO) [a]+-- | Creates a @\<select>@ tag for selecting multiple options.+multiSelectField :: Eq a+ => HandlerFor site (OptionList a)+ -> Field (HandlerFor site) [a] multiSelectField ioptlist = Field parse view UrlEncoded where@@ -417,21 +504,24 @@ optselected (Left _) _ = False optselected (Right vals) opt = (optionInternalValue opt) `elem` vals +-- | Creates an input with @type="radio"@ for selecting one option. radioFieldList :: (Eq a, RenderMessage site FormMessage, RenderMessage site msg) => [(msg, a)]- -> Field (HandlerT site IO) a+ -> Field (HandlerFor site) a radioFieldList = radioField . optionsPairs -checkboxesFieldList :: (Eq a, RenderMessage site FormMessage, RenderMessage site msg) => [(msg, a)]- -> Field (HandlerT site IO) [a]+-- | Creates an input with @type="checkbox"@ for selecting multiple options.+checkboxesFieldList :: (Eq a, RenderMessage site msg) => [(msg, a)]+ -> Field (HandlerFor site) [a] checkboxesFieldList = checkboxesField . optionsPairs -checkboxesField :: (Eq a, RenderMessage site FormMessage)- => HandlerT site IO (OptionList a)- -> Field (HandlerT site IO) [a]+-- | Creates an input with @type="checkbox"@ for selecting multiple options.+checkboxesField :: Eq a+ => HandlerFor site (OptionList a)+ -> Field (HandlerFor site) [a] checkboxesField ioptlist = (multiSelectField ioptlist) { fieldView =- \theId name attrs val isReq -> do+ \theId name attrs val _isReq -> do opts <- fmap olOptions $ handlerToWidget ioptlist let optselected (Left _) _ = False optselected (Right vals) opt = (optionInternalValue opt) `elem` vals@@ -444,29 +534,105 @@ |] } +-- | Creates an input with @type="checkbox"@ for selecting multiple options.+checkboxesField' :: Eq a+ => HandlerFor site (OptionList a)+ -> Field (HandlerFor site) [a]+checkboxesField' ioptlist = (multiSelectField ioptlist)+ { fieldView =+ \theId name attrs val _isReq -> do+ opts <- olOptions <$> handlerToWidget ioptlist+ let optselected (Left _) _ = False+ optselected (Right vals) opt = optionInternalValue opt `elem` vals+ [whamlet|+ <span ##{theId}>+ $forall opt <- opts+ <input id=#{theId}-#{optionExternalValue opt} type=checkbox name=#{name} value=#{optionExternalValue opt} *{attrs} :optselected val opt:checked>+ <label for=#{theId}-#{optionExternalValue opt}>+ #{optionDisplay opt}+ |]+ }+++-- | Creates an input with @type="radio"@ for selecting one option. radioField :: (Eq a, RenderMessage site FormMessage)- => HandlerT site IO (OptionList a)- -> Field (HandlerT site IO) a-radioField = selectFieldHelper- (\theId _name _attrs inside -> [whamlet|+ => HandlerFor site (OptionList a)+ -> Field (HandlerFor site) a+radioField = withRadioField+ (\theId optionWidget -> [whamlet| $newline never-<div ##{theId}>^{inside}+<div .radio>+ <label for=#{theId}-none>+ <div>+ ^{optionWidget}+ _{MsgSelectNone} |])- (\theId name isSel -> [whamlet|+ (\theId value _isSel text optionWidget -> [whamlet| $newline never-<label .radio for=#{theId}-none>- <div>- <input id=#{theId}-none type=radio name=#{name} value=none :isSel:checked>- _{MsgSelectNone}+<div .radio>+ <label for=#{theId}-#{value}>+ <div>+ ^{optionWidget}+ \#{text} |])- (\theId name attrs value isSel text -> [whamlet|+++-- | Creates an input with @type="radio"@ for selecting one option.+--+-- @since 1.7.8+radioField' :: (Eq a, RenderMessage site FormMessage)+ => HandlerFor site (OptionList a)+ -> Field (HandlerFor site) a+radioField' = withRadioField+ (\theId optionWidget -> [whamlet| $newline never-<label .radio for=#{theId}-#{value}>- <div>- <input id=#{theId}-#{value} type=radio name=#{name} value=#{value} :isSel:checked *{attrs}>- \#{text}+<.radio>+ ^{optionWidget}+ <label for=#{theId}-none>+ _{MsgSelectNone} |])+ (\theId value _isSel text optionWidget -> [whamlet|+$newline never+<.radio>+ ^{optionWidget}+ <label for=#{theId}-#{value}>+ \#{text}+|]) +-- | Allows the user to place the option radio widget somewhere in+-- the template.+-- For example: If you want a table of radio options to select.+-- 'radioField' is an example on how to use this function.+--+-- @since 1.7.2+withRadioField :: (Eq a, RenderMessage site FormMessage)+ => (Text -> WidgetFor site ()-> WidgetFor site ()) -- ^ nothing case for mopt+ -> (Text -> Text -> Bool -> Text -> WidgetFor site () -> WidgetFor site ()) -- ^ cases for values+ -> HandlerFor site (OptionList a)+ -> Field (HandlerFor site) a+withRadioField nothingFun optFun =+ selectFieldHelper outside onOpt inside Nothing+ where+ outside theId _name _attrs inside' = [whamlet|+$newline never+<div ##{theId}>^{inside'}+|]+ onOpt theId name isSel = nothingFun theId $ [whamlet|+$newline never+<input id=#{theId}-none type=radio name=#{name} value=none :isSel:checked>+|]+ inside theId name attrs value isSel display =+ optFun theId value isSel display [whamlet|+<input id=#{theId}-#{(value)} type=radio name=#{name} value=#{(value)} :isSel:checked *{attrs}>+|]++-- | Creates a group of radio buttons to answer the question given in the message. Radio buttons are used to allow differentiating between an empty response (@Nothing@) and a no response (@Just False@). Consider using the simpler 'checkBoxField' if you don't need to make this distinction.+--+-- If this field is optional, the first radio button is labeled "\<None>", the second \"Yes" and the third \"No".+--+-- If this field is required, the first radio button is labeled \"Yes" and the second \"No".+--+-- (Exact label titles will depend on localization). boolField :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m Bool boolField = Field { fieldParse = \e _ -> return $ boolParser e@@ -498,14 +664,15 @@ t -> Left $ SomeMessage $ MsgInvalidBool t showVal = either (\_ -> False) --- | While the default @'boolField'@ implements a radio button so you--- can differentiate between an empty response (Nothing) and a no--- response (Just False), this simpler checkbox field returns an empty--- response as Just False.+-- | Creates an input with @type="checkbox"@.+-- While the default @'boolField'@ implements a radio button so you+-- can differentiate between an empty response (@Nothing@) and a no+-- response (@Just False@), this simpler checkbox field returns an empty+-- response as @Just False@. -- -- Note that this makes the field always optional. ---checkBoxField :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m Bool+checkBoxField :: Monad m => Field m Bool checkBoxField = Field { fieldParse = \e _ -> return $ checkBoxParser e , fieldView = \theId name attrs val _ -> [whamlet|@@ -524,23 +691,60 @@ showVal = either (\_ -> False) -data OptionList a = OptionList+-- | A structure holding a list of options. Typically you can use a convenience function like 'mkOptionList' or 'optionsPairs' instead of creating this directly.+--+-- Extended by 'OptionListGrouped' in 1.7.0.+data OptionList a+ = OptionList { olOptions :: [Option a]- , olReadExternal :: Text -> Maybe a+ , olReadExternal :: Text -> Maybe a -- ^ A function mapping from the form's value ('optionExternalValue') to the selected Haskell value ('optionInternalValue'). }+ | OptionListGrouped+ { olOptionsGrouped :: [(Text, [Option a])]+ , olReadExternalGrouped :: Text -> Maybe a -- ^ A function mapping from the form's value ('optionExternalValue') to the selected Haskell value ('optionInternalValue').+ } +-- | Convert grouped 'OptionList' to a normal one.+--+-- @since 1.7.0+flattenOptionList :: OptionList a -> OptionList a+flattenOptionList (OptionListGrouped os re) = OptionList (concatMap snd os) re+flattenOptionList ol = ol++-- | @since 1.4.6+instance Functor OptionList where+ fmap f (OptionList options readExternal) =+ OptionList ((fmap.fmap) f options) (fmap f . readExternal)+ fmap f (OptionListGrouped options readExternal) =+ OptionListGrouped (map (\(g, os) -> (g, (fmap.fmap) f os)) options) (fmap f . readExternal)++-- | Creates an 'OptionList', using a 'Map' to implement the 'olReadExternal' function. mkOptionList :: [Option a] -> OptionList a mkOptionList os = OptionList { olOptions = os , olReadExternal = flip Map.lookup $ Map.fromList $ map (optionExternalValue &&& optionInternalValue) os } +-- | Creates an 'OptionList', using a 'Map' to implement the 'olReadExternalGrouped' function.+--+-- @since 1.7.0+mkOptionListGrouped :: [(Text, [Option a])] -> OptionList a+mkOptionListGrouped os = OptionListGrouped+ { olOptionsGrouped = os+ , olReadExternalGrouped = flip Map.lookup $ Map.fromList $ map (optionExternalValue &&& optionInternalValue) $ concatMap snd os+ }+ data Option a = Option- { optionDisplay :: Text- , optionInternalValue :: a- , optionExternalValue :: Text+ { optionDisplay :: Text -- ^ The user-facing label.+ , optionInternalValue :: a -- ^ The Haskell value being selected.+ , optionExternalValue :: Text -- ^ The representation of this value stored in the form. } +-- | @since 1.4.6+instance Functor Option where+ fmap f (Option display internal external) = Option display (f internal) external++-- | Creates an 'OptionList' from a list of (display-value, internal value) pairs. optionsPairs :: (MonadHandler m, RenderMessage (HandlerSite m) msg) => [(msg, a)] -> m (OptionList a) optionsPairs opts = do@@ -552,19 +756,73 @@ } return $ mkOptionList (zipWith mkOption [1 :: Int ..] opts) +-- | Creates an 'OptionList' from a list of (display-value, internal value) pairs.+--+-- @since 1.7.0+optionsPairsGrouped+ :: forall m msg a. (MonadHandler m, RenderMessage (HandlerSite m) msg)+ => [(msg, [(msg, a)])] -> m (OptionList a)+optionsPairsGrouped opts = do+ mr <- getMessageRender+ let mkOption (external, (display, internal)) =+ Option { optionDisplay = mr display+ , optionInternalValue = internal+ , optionExternalValue = pack $ show external+ }+ opts' = enumerateSublists opts :: [(msg, [(Int, (msg, a))])]+ opts'' = map (\(x, ys) -> (mr x, map mkOption ys)) opts'+ return $ mkOptionListGrouped opts''++-- | Helper to enumerate sublists with one consecutive index.+enumerateSublists :: forall a b. [(a, [b])] -> [(a, [(Int, b)])]+enumerateSublists xss =+ let yss :: [(Int, (a, [b]))]+ yss = snd $ foldl (\(i, res) xs -> (i + (length.snd) xs, res ++ [(i, xs)])) (1, []) xss+ in map (\(i, (x, ys)) -> (x, zip [i :: Int ..] ys)) yss++-- | Creates an 'OptionList' from an 'Enum', using its 'Show' instance for the user-facing value. optionsEnum :: (MonadHandler m, Show a, Enum a, Bounded a) => m (OptionList a) optionsEnum = optionsPairs $ map (\x -> (pack $ show x, x)) [minBound..maxBound] +-- | Selects a list of 'Entity's with the given 'Filter' and 'SelectOpt's. The @(a -> msg)@ function is then used to derive the display value for an 'OptionList'. Example usage:+--+-- > Country+-- > name Text+-- > deriving Eq -- Must derive Eq+--+-- > data CountryForm = CountryForm+-- > { country :: Entity Country+-- > }+-- >+-- > countryNameForm :: AForm Handler CountryForm+-- > countryNameForm = CountryForm+-- > <$> areq (selectField countries) "Which country do you live in?" Nothing+-- > where+-- > countries = optionsPersist [] [Asc CountryName] countryName+#if MIN_VERSION_persistent(2,5,0)+optionsPersist :: ( YesodPersist site+ , PersistQueryRead backend+ , PathPiece (Key a)+ , RenderMessage site msg+ , YesodPersistBackend site ~ backend+ , PersistRecordBackend a backend+ )+ => [Filter a]+ -> [SelectOpt a]+ -> (a -> msg)+ -> HandlerFor site (OptionList (Entity a))+#else optionsPersist :: ( YesodPersist site, PersistEntity a- , PersistQuery (YesodPersistBackend site (HandlerT site IO))+ , PersistQuery (PersistEntityBackend a) , PathPiece (Key a)- , PersistEntityBackend a ~ PersistMonadBackend (YesodPersistBackend site (HandlerT site IO)) , RenderMessage site msg+ , YesodPersistBackend site ~ PersistEntityBackend a ) => [Filter a] -> [SelectOpt a] -> (a -> msg)- -> HandlerT site IO (OptionList (Entity a))+ -> HandlerFor site (OptionList (Entity a))+#endif optionsPersist filts ords toDisplay = fmap mkOptionList $ do mr <- getMessageRender pairs <- runDB $ selectList filts ords@@ -574,21 +832,37 @@ , optionExternalValue = toPathPiece key }) pairs --- | An alternative to 'optionsPersist' which returns just the @Key@ instead of--- the entire @Entity@.+-- | An alternative to 'optionsPersist' which returns just the 'Key' instead of+-- the entire 'Entity'. ----- Since 1.3.2+-- @since 1.3.2+#if MIN_VERSION_persistent(2,5,0) optionsPersistKey :: (YesodPersist site+ , PersistQueryRead backend+ , PathPiece (Key a)+ , RenderMessage site msg+ , backend ~ YesodPersistBackend site+ , PersistRecordBackend a backend+ )+ => [Filter a]+ -> [SelectOpt a]+ -> (a -> msg)+ -> HandlerFor site (OptionList (Key a))+#else+optionsPersistKey+ :: (YesodPersist site , PersistEntity a- , PersistQuery (YesodPersistBackend site (HandlerT site IO))+ , PersistQuery (PersistEntityBackend a) , PathPiece (Key a) , RenderMessage site msg- , PersistEntityBackend a ~ PersistMonadBackend (YesodPersistBackend site (HandlerT site IO)))+ , YesodPersistBackend site ~ PersistEntityBackend a+ ) => [Filter a] -> [SelectOpt a] -> (a -> msg)- -> HandlerT site IO (OptionList (Key a))+ -> HandlerFor site (OptionList (Key a))+#endif optionsPersistKey filts ords toDisplay = fmap mkOptionList $ do mr <- getMessageRender@@ -599,32 +873,39 @@ , optionExternalValue = toPathPiece key }) pairs +-- |+-- A helper function for constucting 'selectField's with optional option groups. You may want to use this when you define your custom 'selectField's or 'radioField's.+--+-- @since 1.6.2 selectFieldHelper :: (Eq a, RenderMessage site FormMessage)- => (Text -> Text -> [(Text, Text)] -> WidgetT site IO () -> WidgetT site IO ())- -> (Text -> Text -> Bool -> WidgetT site IO ())- -> (Text -> Text -> [(Text, Text)] -> Text -> Bool -> Text -> WidgetT site IO ())- -> HandlerT site IO (OptionList a)- -> Field (HandlerT site IO) a-selectFieldHelper outside onOpt inside opts' = Field+ => (Text -> Text -> [(Text, Text)] -> WidgetFor site () -> WidgetFor site ()) -- ^ Outermost part of the field+ -> (Text -> Text -> Bool -> WidgetFor site ()) -- ^ An option for None if the field is optional+ -> (Text -> Text -> [(Text, Text)] -> Text -> Bool -> Text -> WidgetFor site ()) -- ^ Other options+ -> (Maybe (Text -> WidgetFor site ())) -- ^ Group headers placed inbetween options+ -> HandlerFor site (OptionList a)+ -> Field (HandlerFor site) a+selectFieldHelper outside onOpt inside grpHdr opts' = Field { fieldParse = \x _ -> do- opts <- opts'+ opts <- fmap flattenOptionList opts' return $ selectParser opts x , fieldView = \theId name attrs val isReq -> do- opts <- fmap olOptions $ handlerToWidget opts' outside theId name attrs $ do- unless isReq $ onOpt theId name $ not $ render opts val `elem` map optionExternalValue opts- flip mapM_ opts $ \opt -> inside- theId- name- ((if isReq then (("required", "required"):) else id) attrs)- (optionExternalValue opt)- ((render opts val) == optionExternalValue opt)- (optionDisplay opt)+ optsFlat <- fmap (olOptions.flattenOptionList) $ handlerToWidget opts'+ unless isReq $ onOpt theId name $ render optsFlat val `notElem` map optionExternalValue optsFlat+ opts'' <- handlerToWidget opts'+ case opts'' of+ OptionList{} -> constructOptions theId name attrs val isReq optsFlat+ OptionListGrouped{olOptionsGrouped=grps} -> do+ forM_ grps $ \(grp, opts) -> do+ case grpHdr of+ Just hdr -> hdr grp+ Nothing -> return ()+ constructOptions theId name attrs val isReq opts , fieldEnctype = UrlEncoded } where- render _ (Left _) = ""+ render _ (Left x) = x render opts (Right a) = maybe "" optionExternalValue $ listToMaybe $ filter ((== a) . optionInternalValue) opts selectParser _ [] = Right Nothing selectParser opts (s:_) = case s of@@ -633,8 +914,17 @@ x -> case olReadExternal opts x of Nothing -> Left $ SomeMessage $ MsgInvalidEntry x Just y -> Right $ Just y+ constructOptions theId name attrs val isReq opts =+ forM_ opts $ \opt -> inside+ theId+ name+ ((if isReq then (("required", "required"):) else id) attrs)+ (optionExternalValue opt)+ (render opts val == optionExternalValue opt)+ (optionDisplay opt) -fileField :: (Monad m, RenderMessage (HandlerSite m) FormMessage)+-- | Creates an input with @type="file"@.+fileField :: Monad m => Field m FileInfo fileField = Field { fieldParse = \_ files -> return $@@ -680,7 +970,6 @@ return (res, (fv :), ints', Multipart) fileAFormOpt :: MonadHandler m- => RenderMessage (HandlerSite m) FormMessage => FieldSettings (HandlerSite m) -> AForm m (Maybe FileInfo) fileAFormOpt fs = AForm $ \(master, langs) menvs ints -> do@@ -729,4 +1018,52 @@ then "-0." `T.append` (T.drop 2 t1) else t1 - where t1 = T.dropWhile ((==) ' ') t0+ where t1 = T.dropWhile (== ' ') t0++-- $optionsOverview+-- These functions create inputs where one or more options can be selected from a list.+--+-- The basic datastructure used is an 'Option', which combines a user-facing display value, the internal Haskell value being selected, and an external 'Text' stored as the @value@ in the form (used to map back to the internal value). A list of these, together with a function mapping from an external value back to a Haskell value, form an 'OptionList', which several of these functions take as an argument.+--+-- Typically, you won't need to create an 'OptionList' directly and can instead make one with functions like 'optionsPairs' or 'optionsEnum'. Alternatively, you can use functions like 'selectFieldList', which use their @[(msg, a)]@ parameter to create an 'OptionList' themselves.++-- | Creates an input with @type="color"@.+-- The input value must be provided in hexadecimal format #rrggbb.+--+-- @since 1.7.1+colorField :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m Text+colorField = Field+ { fieldParse = parseHelper $ \s ->+ if isHexColor $ unpack s then Right s+ else Left $ MsgInvalidHexColorFormat s+ , fieldView = \theId name attrs val _ -> [whamlet|+$newline never+<input ##{theId} name=#{name} *{attrs} type=color value=#{either id id val}>+|]+ , fieldEnctype = UrlEncoded+ }+ where+ isHexColor :: String -> Bool+ isHexColor ['#',a,b,c,d,e,f] = all isHexDigit [a,b,c,d,e,f]+ isHexColor _ = False++-- | Creates an input with @type="datetime-local"@.+-- The input value must be provided in YYYY-MM-DD(T| )HH:MM[:SS] format.+--+-- @since 1.7.6+datetimeLocalField :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m LocalTime+datetimeLocalField = Field+ { fieldParse = parseHelper $ \s -> case T.split (\c -> (c == 'T') || (c == ' ')) s of+ [d,t] -> do+ day <- parseDate $ unpack d+ time <- parseTime t+ Right $ LocalTime day time+ _ -> Left $ MsgInvalidDatetimeFormat s+ , fieldView = \theId name attrs val isReq -> [whamlet|+$newline never+<input type=datetime-local ##{theId} name=#{name} value=#{showVal val} *{attrs} :isReq:required>+|]+ , fieldEnctype = UrlEncoded+ }+ where+ showVal = either id (pack . show)
Yesod/Form/Functions.hs view
@@ -1,10 +1,12 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE CPP #-}+{-# LANGUAGE TypeOperators #-}+ module Yesod.Form.Functions ( -- * Running in MForm monad newFormIdent@@ -13,10 +15,18 @@ -- * Applicative/Monadic conversion , formToAForm , aFormToForm+ , mFormToWForm+ , wFormToAForm+ , wFormToMForm -- * Fields to Forms+ , wreq+ , wreqMsg+ , wopt , mreq+ , mreqMsg , mopt , areq+ , areqMsg , aopt -- * Run a form , runFormPost@@ -45,13 +55,18 @@ , fieldSettingsLabel , parseHelper , parseHelperGen+ , convertField+ , addClass+ , removeClass ) where import Yesod.Form.Types import Data.Text (Text, pack)+import qualified Data.Text as T import Control.Arrow (second)-import Control.Monad.Trans.RWS (ask, get, put, runRWST, tell, evalRWST, local) import Control.Monad.Trans.Class+import Control.Monad.Trans.RWS (ask, get, put, runRWST, tell, evalRWST, local, mapRWST)+import Control.Monad.Trans.Writer (runWriterT, writer) import Control.Monad (liftM, join) import Data.Byteable (constEqBytes) import Text.Blaze (Markup, toMarkup)@@ -59,8 +74,6 @@ #define toHtml toMarkup import Yesod.Core import Network.Wai (requestMethod)-import Text.Hamlet (shamlet)-import Data.Monoid (mempty) import Data.Maybe (listToMaybe, fromMaybe) import qualified Data.Map as Map import qualified Data.Text.Encoding as TE@@ -105,6 +118,74 @@ (x, _, _) <- ask return $ liftM snd x +-- | Converts a form field into monadic form 'WForm'. This field requires a+-- value and will return 'FormFailure' if left empty.+--+-- @since 1.4.14+wreq :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)+ => Field m a -- ^ form field+ -> FieldSettings site -- ^ settings for this field+ -> Maybe a -- ^ optional default value+ -> WForm m (FormResult a)+wreq f fs = wreqMsg f fs MsgValueRequired++-- | Same as @wreq@ but with your own message to be rendered in case the value+-- is not provided.+--+-- This is useful when you have several required fields on the page and you+-- want to differentiate between which fields were left blank. Otherwise the+-- user sees "Value is required" multiple times, which is ambiguous.+--+-- @since 1.6.7+wreqMsg :: (RenderMessage site msg, HandlerSite m ~ site, MonadHandler m)+ => Field m a -- ^ form field+ -> FieldSettings site -- ^ settings for this field+ -> msg -- ^ message to use in case value is Nothing+ -> Maybe a -- ^ optional default value+ -> WForm m (FormResult a)+wreqMsg f fs msg = mFormToWForm . mreqMsg f fs msg++-- | Converts a form field into monadic form 'WForm'. This field is optional,+-- i.e. if filled in, it returns 'Just a', if left empty, it returns+-- 'Nothing'. Arguments are the same as for 'wreq' (apart from type of default+-- value).+--+-- @since 1.4.14+wopt :: (MonadHandler m, HandlerSite m ~ site)+ => Field m a -- ^ form field+ -> FieldSettings site -- ^ settings for this field+ -> Maybe (Maybe a) -- ^ optional default value+ -> WForm m (FormResult (Maybe a))+wopt f fs = mFormToWForm . mopt f fs++-- | Converts a monadic form 'WForm' into an applicative form 'AForm'.+--+-- @since 1.4.14+wFormToAForm :: MonadHandler m+ => WForm m (FormResult a) -- ^ input form+ -> AForm m a -- ^ output form+wFormToAForm = formToAForm . wFormToMForm++-- | Converts a monadic form 'WForm' into another monadic form 'MForm'.+--+-- @since 1.4.14+wFormToMForm :: (MonadHandler m, HandlerSite m ~ site)+ => WForm m a -- ^ input form+ -> MForm m (a, [FieldView site]) -- ^ output form+wFormToMForm = mapRWST (fmap group . runWriterT)+ where+ group ((a, ints, enctype), views) = ((a, views), ints, enctype)++-- | Converts a monadic form 'MForm' into another monadic form 'WForm'.+--+-- @since 1.4.14+mFormToWForm :: (MonadHandler m, HandlerSite m ~ site)+ => MForm m (a, FieldView site) -- ^ input form+ -> WForm m a -- ^ output form+mFormToWForm = mapRWST $ \f -> do+ ((a, view), ints, enctype) <- lift f+ writer ((a, ints, enctype), [view])+ -- | Converts a form field into monadic form. This field requires a value -- and will return 'FormFailure' if left empty. mreq :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)@@ -112,8 +193,25 @@ -> FieldSettings site -- ^ settings for this field -> Maybe a -- ^ optional default value -> MForm m (FormResult a, FieldView site)-mreq field fs mdef = mhelper field fs mdef (\m l -> FormFailure [renderMessage m l MsgValueRequired]) FormSuccess True+mreq field fs mdef = mreqMsg field fs MsgValueRequired mdef +-- | Same as @mreq@ but with your own message to be rendered in case the value+-- is not provided.+--+-- This is useful when you have several required fields on the page and you+-- want to differentiate between which fields were left blank. Otherwise the+-- user sees "Value is required" multiple times, which is ambiguous.+--+-- @since 1.6.6+mreqMsg :: (RenderMessage site msg, HandlerSite m ~ site, MonadHandler m)+ => Field m a -- ^ form field+ -> FieldSettings site -- ^ settings for this field+ -> msg -- ^ Message to use in case value is Nothing+ -> Maybe a -- ^ optional default value+ -> MForm m (FormResult a, FieldView site)+mreqMsg field fs msg mdef = mhelper field fs mdef formFailure FormSuccess True+ where formFailure m l = FormFailure [renderMessage m l msg]+ -- | Converts a form field into monadic form. This field is optional, i.e. -- if filled in, it returns 'Just a', if left empty, it returns 'Nothing'. -- Arguments are the same as for 'mreq' (apart from type of default value).@@ -168,12 +266,28 @@ -- | Applicative equivalent of 'mreq'. areq :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)- => Field m a- -> FieldSettings site- -> Maybe a+ => Field m a -- ^ form field+ -> FieldSettings site -- ^ settings for this field+ -> Maybe a -- ^ optional default value -> AForm m a-areq a b = formToAForm . liftM (second return) . mreq a b+areq f fs = areqMsg f fs MsgValueRequired +-- | Same as @areq@ but with your own message to be rendered in case the value+-- is not provided.+--+-- This is useful when you have several required fields on the page and you+-- want to differentiate between which fields were left blank. Otherwise the+-- user sees "Value is required" multiple times, which is ambiguous.+--+-- @since 1.6.7+areqMsg :: (RenderMessage site msg, HandlerSite m ~ site, MonadHandler m)+ => Field m a -- ^ form field+ -> FieldSettings site -- ^ settings for this field+ -> msg -- ^ message to use in case value is Nothing+ -> Maybe a -- ^ optional default value+ -> AForm m a+areqMsg f fs msg = formToAForm . liftM (second return) . mreqMsg f fs msg+ -- | Applicative equivalent of 'mopt'. aopt :: MonadHandler m => Field m a@@ -212,7 +326,7 @@ -> m ((FormResult a, xml), Enctype) postHelper form env = do req <- getRequest- let tokenKey = "_token"+ let tokenKey = defaultCsrfParamName let token = case reqToken req of Nothing -> mempty@@ -222,13 +336,15 @@ ((res, xml), enctype) <- runFormGeneric (form token) m langs env let res' = case (res, env) of+ (_, Nothing) -> FormMissing (FormSuccess{}, Just (params, _)) | not (Map.lookup tokenKey params === reqToken req) -> FormFailure [renderMessage m langs MsgCsrfWarning] _ -> res+ -- It's important to use constant-time comparison (constEqBytes) in order to avoid timing attacks. where (Just [t1]) === (Just t2) = TE.encodeUtf8 t1 `constEqBytes` TE.encodeUtf8 t2- Nothing === Nothing = True -- It's important to use constTimeEq- _ === _ = False -- in order to avoid timing attacks.+ Nothing === Nothing = True+ _ === _ = False return ((res', xml), enctype) -- | Similar to 'runFormPost', except it always ignores the currently available@@ -241,8 +357,7 @@ -> m (xml, Enctype) generateFormPost form = first snd `liftM` postHelper form Nothing -postEnv :: (MonadHandler m, MonadResource m)- => m (Maybe (Env, FileEnv))+postEnv :: MonadHandler m => m (Maybe (Env, FileEnv)) postEnv = do req <- getRequest if requestMethod (reqWaiRequest req) == "GET"@@ -253,8 +368,8 @@ return $ Just (p', Map.unionsWith (++) $ map (\(k, v) -> Map.singleton k [v]) f) runFormPostNoToken :: MonadHandler m- => (Html -> MForm m (FormResult a, xml))- -> m ((FormResult a, xml), Enctype)+ => (Html -> MForm m a)+ -> m (a, Enctype) runFormPostNoToken form = do langs <- languages m <- getYesod@@ -272,17 +387,17 @@ Just _ -> Just (Map.unionsWith (++) $ map (\(x, y) -> Map.singleton x [y]) gets, Map.empty) getHelper form env -{- FIXME: generateFormGet' "Will be renamed to generateFormGet in next verison of Yesod" -}+{- FIXME: generateFormGet' "Will be renamed to generateFormGet in next version of Yesod" -} -- | -- -- Since 1.3.11 generateFormGet'- :: (RenderMessage (HandlerSite m) FormMessage, MonadHandler m)+ :: MonadHandler m => (Html -> MForm m (FormResult a, xml)) -> m (xml, Enctype) generateFormGet' form = first snd `liftM` getHelper form Nothing -{-# DEPRECATED generateFormGet "Will require RenderMessage in next verison of Yesod" #-}+{-# DEPRECATED generateFormGet "Will require RenderMessage in next version of Yesod" #-} generateFormGet :: MonadHandler m => (Html -> MForm m a) -> m (a, Enctype)@@ -326,19 +441,19 @@ identifyForm :: Monad m => Text -- ^ Form identification string.- -> (Html -> MForm m (FormResult a, WidgetT (HandlerSite m) IO ()))- -> (Html -> MForm m (FormResult a, WidgetT (HandlerSite m) IO ()))+ -> (Html -> MForm m (FormResult a, WidgetFor (HandlerSite m) ()))+ -> (Html -> MForm m (FormResult a, WidgetFor (HandlerSite m) ())) identifyForm identVal form = \fragment -> do -- Create hidden <input>. let fragment' = [shamlet|- <input type=hidden name=#{identifyFormKey} value=#{identVal}>+ <input type=hidden name=#{identifyFormKey} value=identify-#{identVal}> #{fragment} |] -- Check if we got its value back. mp <- askParams- let missing = (mp >>= Map.lookup identifyFormKey) /= Just [identVal]+ let missing = (mp >>= Map.lookup identifyFormKey) /= Just ["identify-" <> identVal] -- Run the form proper (with our hidden <input>). If the -- data is missing, then do not provide any params to the@@ -346,8 +461,12 @@ -- doing this avoids having lots of fields with red errors. let eraseParams | missing = local (\(_, h, l) -> (Nothing, h, l)) | otherwise = id- eraseParams (form fragment')+ ( res', w) <- eraseParams (form fragment') + -- Empty forms now properly return FormMissing. [#1072](https://github.com/yesodweb/yesod/issues/1072)+ let res = if missing then FormMissing else res'+ return ( res, w)+ identifyFormKey :: Text identifyFormKey = "_formid" @@ -355,7 +474,7 @@ type FormRender m a = AForm m a -> Html- -> MForm m (FormResult a, WidgetT (HandlerSite m) IO ())+ -> MForm m (FormResult a, WidgetFor (HandlerSite m) ()) renderTable, renderDivs, renderDivsNoLabels :: Monad m => FormRender m a -- | Render a form into a series of tr tags. Note that, in order to allow@@ -526,3 +645,59 @@ parseHelperGen _ [] _ = return $ Right Nothing parseHelperGen _ ("":_) _ = return $ Right Nothing parseHelperGen f (x:_) _ = return $ either (Left . SomeMessage) (Right . Just) $ f x++-- | Since a 'Field' cannot be a 'Functor', it is not obvious how to "reuse" a Field+-- on a @newtype@ or otherwise equivalent type. This function allows you to convert+-- a @Field m a@ to a @Field m b@ assuming you provide a bidirectional+-- conversion between the two, through the first two functions.+--+-- A simple example:+--+-- > import Data.Monoid+-- > sumField :: (Functor m, Monad m, RenderMessage (HandlerSite m) FormMessage) => Field m (Sum Int)+-- > sumField = convertField Sum getSum intField+--+-- Another example, not using a newtype, but instead creating a Lazy Text field:+--+-- > import qualified Data.Text.Lazy as TL+-- > TextField :: (Functor m, Monad m, RenderMessage (HandlerSite m) FormMessage) => Field m TL.Text+-- > lazyTextField = convertField TL.fromStrict TL.toStrict textField+--+-- Since 1.3.16+convertField :: (Functor m)+ => (a -> b) -> (b -> a)+ -> Field m a -> Field m b+convertField to from (Field fParse fView fEnctype) = let+ fParse' ts = fmap (fmap (fmap to)) . fParse ts+ fView' ti tn at ei = fView ti tn at (fmap from ei)+ in Field fParse' fView' fEnctype++-- | Removes a CSS class from the 'fsAttrs' in a 'FieldSettings'.+--+-- ==== __Examples__+--+-- >>> removeClass "form-control" [("class","form-control login-form"),("id","home-login")]+-- [("class"," login-form"),("id","home-login")]+--+-- @since 1.6.2+removeClass :: Text -- ^ The class to remove+ -> [(Text, Text)] -- ^ List of existing 'fsAttrs'+ -> [(Text, Text)]+removeClass _ [] = []+removeClass klass (("class", old):rest) = ("class", T.replace klass " " old) : rest+removeClass klass (other :rest) = other : removeClass klass rest++-- | Adds a CSS class to the 'fsAttrs' in a 'FieldSettings'.+--+-- ==== __Examples__+--+-- >>> addClass "login-form" [("class", "form-control"), ("id", "home-login")]+-- [("class","form-control login-form"),("id","home-login")]+--+-- @since 1.6.2+addClass :: Text -- ^ The class to add+ -> [(Text, Text)] -- ^ List of existing 'fsAttrs'+ -> [(Text, Text)]+addClass klass [] = [("class", klass)]+addClass klass (("class", old):rest) = ("class", T.concat [old, " ", klass]) : rest+addClass klass (other :rest) = other : addClass klass rest
+ Yesod/Form/I18n/Chinese.hs view
@@ -0,0 +1,28 @@+{-# LANGUAGE OverloadedStrings #-}++module Yesod.Form.I18n.Chinese where++import Yesod.Form.Types (FormMessage (..))+import Data.Text (Text)++chineseFormMessage :: FormMessage -> Text+chineseFormMessage (MsgInvalidInteger t) = "无效的整数: " `mappend` t+chineseFormMessage (MsgInvalidNumber t) = "无效的数字: " `mappend` t+chineseFormMessage (MsgInvalidEntry t) = "无效的条目: " `mappend` t+chineseFormMessage MsgInvalidTimeFormat = "无效的时间, 必须符合HH:MM[:SS]格式"+chineseFormMessage MsgInvalidDay = "无效的日期, 必须符合YYYY-MM-DD格式"+chineseFormMessage (MsgInvalidUrl t) = "无效的链接: " `mappend` t+chineseFormMessage (MsgInvalidEmail t) = "无效的邮箱地址: " `mappend` t+chineseFormMessage (MsgInvalidHour t) = "无效的小时: " `mappend` t+chineseFormMessage (MsgInvalidMinute t) = "无效的分钟: " `mappend` t+chineseFormMessage (MsgInvalidSecond t) = "无效的秒: " `mappend` t+chineseFormMessage MsgCsrfWarning = "为了防备跨站请求伪造, 请确认表格提交."+chineseFormMessage MsgValueRequired = "此项必填"+chineseFormMessage (MsgInputNotFound t) = "输入找不到: " `mappend` t+chineseFormMessage MsgSelectNone = "<空>"+chineseFormMessage (MsgInvalidBool t) = "无效的逻辑值: " `mappend` t+chineseFormMessage MsgBoolYes = "是"+chineseFormMessage MsgBoolNo = "否"+chineseFormMessage MsgDelete = "删除?"+chineseFormMessage (MsgInvalidHexColorFormat t) = "颜色无效,必须为 #rrggbb 十六进制格式: " `mappend` t+chineseFormMessage (MsgInvalidDatetimeFormat t) = "日期時間無效,必須採用 YYYY-MM-DD(T| )HH:MM[:SS] 格式: " `mappend` t
Yesod/Form/I18n/Czech.hs view
@@ -1,8 +1,8 @@ {-# LANGUAGE OverloadedStrings #-}+ module Yesod.Form.I18n.Czech where import Yesod.Form.Types (FormMessage (..))-import Data.Monoid (mappend) import Data.Text (Text) czechFormMessage :: FormMessage -> Text@@ -24,3 +24,5 @@ czechFormMessage MsgBoolYes = "Ano" czechFormMessage MsgBoolNo = "Ne" czechFormMessage MsgDelete = "Smazat?"+czechFormMessage (MsgInvalidHexColorFormat t) = "Neplatná barva, musí být v #rrggbb hexadecimálním formátu: " `mappend` t+czechFormMessage (MsgInvalidDatetimeFormat t) = "Neplatné datum a čas, musí být ve formátu YYYY-MM-DD(T| )HH:MM[:SS]: " `mappend` t
+ Yesod/Form/I18n/Dutch.hs view
@@ -0,0 +1,28 @@+{-# LANGUAGE OverloadedStrings #-}++module Yesod.Form.I18n.Dutch where++import Yesod.Form.Types (FormMessage (..))+import Data.Text (Text)++dutchFormMessage :: FormMessage -> Text+dutchFormMessage (MsgInvalidInteger t) = "Ongeldig aantal: " `mappend` t+dutchFormMessage (MsgInvalidNumber t) = "Ongeldig getal: " `mappend` t+dutchFormMessage (MsgInvalidEntry t) = "Ongeldige invoer: " `mappend` t+dutchFormMessage MsgInvalidTimeFormat = "Ongeldige tijd, het juiste formaat is (UU:MM[:SS])"+dutchFormMessage MsgInvalidDay = "Ongeldige datum, het juiste formaat is (JJJJ-MM-DD)"+dutchFormMessage (MsgInvalidUrl t) = "Ongeldige URL: " `mappend` t+dutchFormMessage (MsgInvalidEmail t) = "Ongeldig e-mail adres: " `mappend` t+dutchFormMessage (MsgInvalidHour t) = "Ongeldig uur: " `mappend` t+dutchFormMessage (MsgInvalidMinute t) = "Ongeldige minuut: " `mappend` t+dutchFormMessage (MsgInvalidSecond t) = "Ongeldige seconde: " `mappend` t+dutchFormMessage MsgCsrfWarning = "Bevestig het indienen van het formulier, dit als veiligheidsmaatregel tegen \"cross-site request forgery\" aanvallen."+dutchFormMessage MsgValueRequired = "Verplicht veld"+dutchFormMessage (MsgInputNotFound t) = "Geen invoer gevonden: " `mappend` t+dutchFormMessage MsgSelectNone = "<Geen>"+dutchFormMessage (MsgInvalidBool t) = "Ongeldige waarheidswaarde: " `mappend` t+dutchFormMessage MsgBoolYes = "Ja"+dutchFormMessage MsgBoolNo = "Nee"+dutchFormMessage MsgDelete = "Verwijderen?"+dutchFormMessage (MsgInvalidHexColorFormat t) = "Ongeldige kleur, moet de hexadecimale indeling #rrggbb hebben: " `mappend` t+dutchFormMessage (MsgInvalidDatetimeFormat t) = "Ongeldige datum/tijd, moet de indeling JJJJ-MM-DD(T| )UU:MM[:SS] hebben: " `mappend` t
Yesod/Form/I18n/English.hs view
@@ -1,8 +1,8 @@ {-# LANGUAGE OverloadedStrings #-}+ module Yesod.Form.I18n.English where import Yesod.Form.Types (FormMessage (..))-import Data.Monoid (mappend) import Data.Text (Text) englishFormMessage :: FormMessage -> Text@@ -24,3 +24,5 @@ englishFormMessage MsgBoolYes = "Yes" englishFormMessage MsgBoolNo = "No" englishFormMessage MsgDelete = "Delete?"+englishFormMessage (MsgInvalidHexColorFormat t) = "Invalid color, must be in #rrggbb hexadecimal format: " `mappend` t+englishFormMessage (MsgInvalidDatetimeFormat t) = "Invalid datetime, must be in YYYY-MM-DD(T| )HH:MM[:SS] format: " `mappend` t
Yesod/Form/I18n/French.hs view
@@ -1,8 +1,8 @@ {-# LANGUAGE OverloadedStrings #-}+ module Yesod.Form.I18n.French (frenchFormMessage) where import Yesod.Form.Types (FormMessage (..))-import Data.Monoid (mappend) import Data.Text (Text) frenchFormMessage :: FormMessage -> Text@@ -24,3 +24,5 @@ frenchFormMessage MsgBoolYes = "Oui" frenchFormMessage MsgBoolNo = "Non" frenchFormMessage MsgDelete = "Détruire ?"+frenchFormMessage (MsgInvalidHexColorFormat t) = "Couleur non valide. doit être au format hexadécimal #rrggbb : " `mappend` t+frenchFormMessage (MsgInvalidDatetimeFormat t) = "Date/heure non valide. doit être au format AAAA-MM-JJ(T| )HH:MM[:SS] : " `mappend` t
Yesod/Form/I18n/German.hs view
@@ -1,8 +1,8 @@ {-# LANGUAGE OverloadedStrings #-}+ module Yesod.Form.I18n.German where import Yesod.Form.Types (FormMessage (..))-import Data.Monoid (mappend) import Data.Text (Text) germanFormMessage :: FormMessage -> Text@@ -24,3 +24,5 @@ germanFormMessage MsgBoolYes = "Ja" germanFormMessage MsgBoolNo = "Nein" germanFormMessage MsgDelete = "Löschen?"+germanFormMessage (MsgInvalidHexColorFormat t) = "Ungültige Farbe, muss im Hexadezimalformat #rrggbb vorliegen: " `mappend` t+germanFormMessage (MsgInvalidDatetimeFormat t) = "Ungültige Datums- und Uhrzeitangabe, muss im Format YYYY-MM-DD(T| )HH:MM[:SS] vorliegen: " `mappend` t
Yesod/Form/I18n/Japanese.hs view
@@ -1,8 +1,8 @@ {-# LANGUAGE OverloadedStrings #-}+ module Yesod.Form.I18n.Japanese where import Yesod.Form.Types (FormMessage (..))-import Data.Monoid (mappend) import Data.Text (Text) japaneseFormMessage :: FormMessage -> Text@@ -24,3 +24,5 @@ japaneseFormMessage MsgBoolYes = "はい" japaneseFormMessage MsgBoolNo = "いいえ" japaneseFormMessage MsgDelete = "削除しますか?"+japaneseFormMessage (MsgInvalidHexColorFormat t) = "無効な色。#rrggbb16進形式である必要があります: " `mappend` t+japaneseFormMessage (MsgInvalidDatetimeFormat t) = "無効な日時です。YYYY-MM-DD(T| )HH:MM[:SS] 形式である必要があります: " `mappend` t
+ Yesod/Form/I18n/Korean.hs view
@@ -0,0 +1,28 @@+{-# LANGUAGE OverloadedStrings #-}++module Yesod.Form.I18n.Korean where++import Yesod.Form.Types (FormMessage (..))+import Data.Text (Text)++koreanFormMessage :: FormMessage -> Text+koreanFormMessage (MsgInvalidInteger t) = "잘못된 정수입니다: " `mappend` t+koreanFormMessage (MsgInvalidNumber t) = "잘못된 숫자입니다: " `mappend` t+koreanFormMessage (MsgInvalidEntry t) = "잘못된 입력입니다: " `mappend` t+koreanFormMessage MsgInvalidTimeFormat = "잘못된 시간입니다. HH:MM[:SS] 형태로 입력하세요"+koreanFormMessage MsgInvalidDay = "잘못된 날짜입니다. YYYY-MM-DD 형태로 입력하세요"+koreanFormMessage (MsgInvalidUrl t) = "잘못된 URL입니다: " `mappend` t+koreanFormMessage (MsgInvalidEmail t) = "잘못된 이메일 주소입니다: " `mappend` t+koreanFormMessage (MsgInvalidHour t) = "잘못된 시간입니다: " `mappend` t+koreanFormMessage (MsgInvalidMinute t) = "잘못된 분입니다: " `mappend` t+koreanFormMessage (MsgInvalidSecond t) = "잘못된 초입니다: " `mappend` t+koreanFormMessage MsgCsrfWarning = "CSRF공격을 방지하기 위해 양식의 입력을 확인하세요."+koreanFormMessage MsgValueRequired = "값은 필수입니다"+koreanFormMessage (MsgInputNotFound t) = "입력을 찾을 수 없습니다: " `mappend` t+koreanFormMessage MsgSelectNone = "<없음>"+koreanFormMessage (MsgInvalidBool t) = "잘못된 불(boolean)입니다: " `mappend` t+koreanFormMessage MsgBoolYes = "예"+koreanFormMessage MsgBoolNo = "아니오"+koreanFormMessage MsgDelete = "삭제하시겠습니까?"+koreanFormMessage (MsgInvalidHexColorFormat t) = "색상이 잘못되었습니다. #rrggbb 16진수 형식이어야 합니다.: " `mappend` t+koreanFormMessage (MsgInvalidDatetimeFormat t) = "날짜/시간이 잘못되었습니다. YYYY-MM-DD(T| )HH:MM[:SS] 형식이어야 합니다.: " `mappend` t
Yesod/Form/I18n/Norwegian.hs view
@@ -1,8 +1,8 @@ {-# LANGUAGE OverloadedStrings #-}+ module Yesod.Form.I18n.Norwegian where import Yesod.Form.Types (FormMessage (..))-import Data.Monoid (mappend) import Data.Text (Text) norwegianBokmålFormMessage :: FormMessage -> Text@@ -24,3 +24,5 @@ norwegianBokmålFormMessage MsgBoolNo = "Nei" norwegianBokmålFormMessage MsgDelete = "Slette?" norwegianBokmålFormMessage MsgCsrfWarning = "Som beskyttelse mot «cross-site request forgery»-angrep, vennligst bekreft innsendt skjema."+norwegianBokmålFormMessage (MsgInvalidHexColorFormat t) = "Ugyldig farge, må være i #rrggbb heksadesimalt format: " `mappend` t+norwegianBokmålFormMessage (MsgInvalidDatetimeFormat t) = "Ugyldig datoklokkeslett, må være i formatet ÅÅÅÅ-MM-DD(T| )HH:MM[:SS]:" `mappend` t
Yesod/Form/I18n/Portuguese.hs view
@@ -1,8 +1,8 @@ {-# LANGUAGE OverloadedStrings #-}+ module Yesod.Form.I18n.Portuguese where import Yesod.Form.Types (FormMessage (..))-import Data.Monoid (mappend) import Data.Text (Text) portugueseFormMessage :: FormMessage -> Text@@ -24,3 +24,5 @@ portugueseFormMessage MsgBoolYes = "Sim" portugueseFormMessage MsgBoolNo = "Não" portugueseFormMessage MsgDelete = "Remover?"+portugueseFormMessage (MsgInvalidHexColorFormat t) = "Cor inválida, deve estar no formato #rrggbb hexadecimal: " `mappend` t+portugueseFormMessage (MsgInvalidDatetimeFormat t) = "Data e hora inválida, deve estar no formato AAAA-MM-DD(T| )HH:MM[:SS]: " `mappend` t
+ Yesod/Form/I18n/Romanian.hs view
@@ -0,0 +1,31 @@+{-# LANGUAGE OverloadedStrings #-}++module Yesod.Form.I18n.Romanian where++import Yesod.Form.Types (FormMessage (..))+import Data.Text (Text)++-- | Romanian translation+--+-- @since 1.7.5+romanianFormMessage :: FormMessage -> Text+romanianFormMessage (MsgInvalidInteger t) = "Număr întreg nevalid: " `mappend` t+romanianFormMessage (MsgInvalidNumber t) = "Număr nevalid: " `mappend` t+romanianFormMessage (MsgInvalidEntry t) = "Valoare nevalidă: " `mappend` t+romanianFormMessage MsgInvalidTimeFormat = "Oră nevalidă. Formatul necesar este HH:MM[:SS]"+romanianFormMessage MsgInvalidDay = "Dată nevalidă. Formatul necesar este AAAA-LL-ZZ"+romanianFormMessage (MsgInvalidUrl t) = "Adresă URL nevalidă: " `mappend` t+romanianFormMessage (MsgInvalidEmail t) = "Adresă de e-mail nevalidă: " `mappend` t+romanianFormMessage (MsgInvalidHour t) = "Oră nevalidă: " `mappend` t+romanianFormMessage (MsgInvalidMinute t) = "Minut nevalid: " `mappend` t+romanianFormMessage (MsgInvalidSecond t) = "Secundă nevalidă: " `mappend` t+romanianFormMessage MsgCsrfWarning = "Ca protecție împotriva atacurilor CSRF, vă rugăm să confirmați trimiterea formularului."+romanianFormMessage MsgValueRequired = "Câmp obligatoriu"+romanianFormMessage (MsgInputNotFound t) = "Valoare inexistentă: " `mappend` t+romanianFormMessage MsgSelectNone = "<Niciuna>"+romanianFormMessage (MsgInvalidBool t) = "Valoare booleană nevalidă: " `mappend` t+romanianFormMessage MsgBoolYes = "Da"+romanianFormMessage MsgBoolNo = "Nu"+romanianFormMessage MsgDelete = "Șterge?"+romanianFormMessage (MsgInvalidHexColorFormat t) = "Culoare nevalidă. Formatul necesar este #rrggbb în hexazecimal: " `mappend` t+romanianFormMessage (MsgInvalidDatetimeFormat t) = "Data și ora nevalidă, trebuie să fie în format AAAA-LL-ZZ(T| )HH:MM[:SS]: " `mappend` t
Yesod/Form/I18n/Russian.hs view
@@ -1,8 +1,8 @@ {-# LANGUAGE OverloadedStrings #-}+ module Yesod.Form.I18n.Russian where import Yesod.Form.Types (FormMessage (..))-import Data.Monoid (mappend) import Data.Text (Text) russianFormMessage :: FormMessage -> Text@@ -15,7 +15,7 @@ russianFormMessage (MsgInvalidEmail t) = "Неверно указана электронная почта: " `mappend` t russianFormMessage (MsgInvalidHour t) = "Неверно указан час: " `mappend` t russianFormMessage (MsgInvalidMinute t) = "Неверно указаны минуты: " `mappend` t-russianFormMessage (MsgInvalidSecond t) = "Неверны указаны секунды: " `mappend` t+russianFormMessage (MsgInvalidSecond t) = "Неверно указаны секунды: " `mappend` t russianFormMessage MsgCsrfWarning = "Для защиты от межсайтовой подделки запросов (CSRF), пожалуйста, подтвердите отправку данных формы." russianFormMessage MsgValueRequired = "Обязательно к заполнению" russianFormMessage (MsgInputNotFound t) = "Поле не найдено: " `mappend` t@@ -24,3 +24,5 @@ russianFormMessage MsgBoolYes = "Да" russianFormMessage MsgBoolNo = "Нет" russianFormMessage MsgDelete = "Удалить?"+russianFormMessage (MsgInvalidHexColorFormat t) = "Недопустимое значение цвета, должен быть в шестнадцатеричном формате #rrggbb: " `mappend` t+russianFormMessage (MsgInvalidDatetimeFormat t) = "Недопустимое значение даты и времени. Должно быть в формате ГГГГ-ММ-ДД(T| )ЧЧ:ММ[:СС]: " `mappend` t
+ Yesod/Form/I18n/Spanish.hs view
@@ -0,0 +1,28 @@+{-# LANGUAGE OverloadedStrings #-}++module Yesod.Form.I18n.Spanish where++import Yesod.Form.Types (FormMessage (..))+import Data.Text (Text)++spanishFormMessage :: FormMessage -> Text+spanishFormMessage (MsgInvalidInteger t) = "Número entero inválido: " `mappend` t+spanishFormMessage (MsgInvalidNumber t) = "Número inválido: " `mappend` t+spanishFormMessage (MsgInvalidEntry t) = "Entrada inválida: " `mappend` t+spanishFormMessage MsgInvalidTimeFormat = "Hora inválida, debe tener el formato HH:MM[:SS]"+spanishFormMessage MsgInvalidDay = "Fecha inválida, debe tener el formato AAAA-MM-DD"+spanishFormMessage (MsgInvalidUrl t) = "URL inválida: " `mappend` t+spanishFormMessage (MsgInvalidEmail t) = "Dirección de correo electrónico inválida: " `mappend` t+spanishFormMessage (MsgInvalidHour t) = "Hora inválida: " `mappend` t+spanishFormMessage (MsgInvalidMinute t) = "Minuto inválido: " `mappend` t+spanishFormMessage (MsgInvalidSecond t) = "Segundo inválido: " `mappend` t+spanishFormMessage MsgCsrfWarning = "Como protección contra ataques CSRF, confirme su envío por favor."+spanishFormMessage MsgValueRequired = "Se requiere un valor"+spanishFormMessage (MsgInputNotFound t) = "Entrada no encontrada: " `mappend` t+spanishFormMessage MsgSelectNone = "<Ninguno>"+spanishFormMessage (MsgInvalidBool t) = "Booleano inválido: " `mappend` t+spanishFormMessage MsgBoolYes = "Sí"+spanishFormMessage MsgBoolNo = "No"+spanishFormMessage MsgDelete = "¿Eliminar?"+spanishFormMessage (MsgInvalidHexColorFormat t) = "Color no válido, debe estar en formato hexadecimal #rrggbb: " `mappend` t+spanishFormMessage (MsgInvalidDatetimeFormat t) = "Fecha y hora no válida; debe estar en formato AAAA-MM-DD(T| )HH:MM[:SS]: " `mappend` t
Yesod/Form/I18n/Swedish.hs view
@@ -1,8 +1,8 @@ {-# LANGUAGE OverloadedStrings #-}+ module Yesod.Form.I18n.Swedish where import Yesod.Form.Types (FormMessage (..))-import Data.Monoid (mappend) import Data.Text (Text) swedishFormMessage :: FormMessage -> Text@@ -24,3 +24,5 @@ swedishFormMessage MsgBoolNo = "Nej" swedishFormMessage MsgDelete = "Radera?" swedishFormMessage MsgCsrfWarning = "Som skydd mot \"cross-site request forgery\" attacker, vänligen bekräfta skickandet av formuläret."+swedishFormMessage (MsgInvalidHexColorFormat t) = "Ogiltig färg, måste vara i #rrggbb hexadecimalt format: " `mappend` t+swedishFormMessage (MsgInvalidDatetimeFormat t) = "Ogiltig datumtid, måste vara i formatet ÅÅÅÅ-MM-DD(T| )TT:MM[:SS]: " `mappend` t
Yesod/Form/Input.hs view
@@ -1,8 +1,13 @@-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}++-- | Provides for getting input from either GET or POST params without+-- generating HTML forms. For more information, see:+-- <http://www.yesodweb.com/book/forms#forms_kinds_of_forms>. module Yesod.Form.Input ( FormInput (..) , runInputGet+ , runInputGetResult , runInputPost , runInputPostResult , ireq@@ -11,14 +16,16 @@ import Yesod.Form.Types import Data.Text (Text)-import Control.Applicative (Applicative (..)) import Yesod.Core-import Control.Monad (liftM)+import Control.Monad (liftM, (<=<)) import qualified Data.Map as Map import Data.Maybe (fromMaybe) import Control.Arrow ((***)) type DText = [Text] -> [Text]++-- | Type for a form which parses a value of type @a@ with the base monad @m@+-- (usually your @Handler@). Can compose this using its @Applicative@ instance. newtype FormInput m a = FormInput { unFormInput :: HandlerSite m -> [Text] -> Env -> FileEnv -> m (Either DText a) } instance Monad m => Functor (FormInput m) where fmap a (FormInput f) = FormInput $ \c d e e' -> liftM (either Left (Right . a)) $ f c d e e'@@ -33,8 +40,12 @@ (_, Left b) -> Left b (Right a, Right b) -> Right $ a b +-- | Promote a @Field@ into a @FormInput@, requiring that the value be present+-- and valid. ireq :: (Monad m, RenderMessage (HandlerSite m) FormMessage)- => Field m a -> Text -> FormInput m a+ => Field m a+ -> Text -- ^ name of the field+ -> FormInput m a ireq field name = FormInput $ \m l env fenv -> do let filteredEnv = fromMaybe [] $ Map.lookup name env filteredFEnv = fromMaybe [] $ Map.lookup name fenv@@ -44,6 +55,8 @@ Right Nothing -> Left $ (:) $ renderMessage m l $ MsgInputNotFound name Right (Just a) -> Right a +-- | Promote a @Field@ into a @FormInput@, with its presence being optional. If+-- the value is present but does not parse correctly, the form will still fail. iopt :: Monad m => Field m a -> Text -> FormInput m (Maybe a) iopt field name = FormInput $ \m l env fenv -> do let filteredEnv = fromMaybe [] $ Map.lookup name env@@ -53,32 +66,38 @@ Left (SomeMessage e) -> Left $ (:) $ renderMessage m l e Right x -> Right x +-- | Run a @FormInput@ on the GET parameters (i.e., query string). If parsing+-- fails, calls 'invalidArgs'. runInputGet :: MonadHandler m => FormInput m a -> m a-runInputGet (FormInput f) = do+runInputGet = either invalidArgs return <=< runInputGetHelper++-- | Run a @FormInput@ on the GET parameters (i.e., query string). Does /not/+-- throw exceptions on failure.+--+-- Since 1.4.1+runInputGetResult :: MonadHandler m => FormInput m a -> m (FormResult a)+runInputGetResult = fmap (either FormFailure FormSuccess) . runInputGetHelper++runInputGetHelper :: MonadHandler m => FormInput m a -> m (Either [Text] a)+runInputGetHelper (FormInput f) = do env <- liftM (toMap . reqGetParams) getRequest m <- getYesod l <- languages emx <- f m l env Map.empty- case emx of- Left errs -> invalidArgs $ errs []- Right x -> return x+ return $ either (Left . ($ [])) Right emx toMap :: [(Text, a)] -> Map.Map Text [a] toMap = Map.unionsWith (++) . map (\(x, y) -> Map.singleton x [y]) +-- | Run a @FormInput@ on the POST parameters (i.e., request body). If parsing+-- fails, calls 'invalidArgs'. runInputPost :: MonadHandler m => FormInput m a -> m a-runInputPost fi = do- emx <- runInputPostHelper fi- case emx of- Left errs -> invalidArgs errs- Right x -> return x+runInputPost = either invalidArgs return <=< runInputPostHelper +-- | Run a @FormInput@ on the POST parameters (i.e., request body). Does /not/+-- throw exceptions on failure. runInputPostResult :: MonadHandler m => FormInput m a -> m (FormResult a)-runInputPostResult fi = do- emx <- runInputPostHelper fi- case emx of- Left errs -> return $ FormFailure errs- Right x -> return $ FormSuccess x+runInputPostResult = fmap (either FormFailure FormSuccess) . runInputPostHelper runInputPostHelper :: MonadHandler m => FormInput m a -> m (Either [Text] a) runInputPostHelper (FormInput f) = do
Yesod/Form/Jquery.hs view
@@ -1,11 +1,14 @@-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+ -- | Some fields spiced up with jQuery UI. module Yesod.Form.Jquery ( YesodJquery (..) , jqueryDayField+ , jqueryDatePickerDayField , jqueryAutocompleteField , jqueryAutocompleteField' , googleHostedJqueryUiCss@@ -17,10 +20,8 @@ import Yesod.Form import Data.Time (Day) import Data.Default-import Text.Hamlet (shamlet)-import Text.Julius (julius, rawJS)+import Text.Julius (rawJS) import Data.Text (Text, pack, unpack)-import Data.Monoid (mconcat) -- | Gets the Google hosted jQuery UI 1.8 CSS file with the given theme. googleHostedJqueryUiCss :: Text -> Text@@ -53,8 +54,17 @@ urlJqueryUiDateTimePicker :: a -> Either (Route a) Text urlJqueryUiDateTimePicker _ = Right "http://github.com/gregwebs/jquery.ui.datetimepicker/raw/master/jquery.ui.datetimepicker.js" -jqueryDayField :: (RenderMessage site FormMessage, YesodJquery site) => JqueryDaySettings -> Field (HandlerT site IO) Day-jqueryDayField jds = Field+jqueryDayField :: (RenderMessage site FormMessage, YesodJquery site) => JqueryDaySettings -> Field (HandlerFor site) Day+jqueryDayField = flip jqueryDayField' "date"++-- | Use jQuery's datepicker as the underlying implementation.+--+-- Since 1.4.3+jqueryDatePickerDayField :: (RenderMessage site FormMessage, YesodJquery site) => JqueryDaySettings -> Field (HandlerFor site) Day+jqueryDatePickerDayField = flip jqueryDayField' "text"++jqueryDayField' :: (RenderMessage site FormMessage, YesodJquery site) => JqueryDaySettings -> Text -> Field (HandlerFor site) Day+jqueryDayField' jds inputType = Field { fieldParse = parseHelper $ maybe (Left MsgInvalidDay) Right@@ -63,7 +73,7 @@ , fieldView = \theId name attrs val isReq -> do toWidget [shamlet| $newline never-<input id="#{theId}" name="#{name}" *{attrs} type="date" :isReq:required="" value="#{showVal val}">+<input id="#{theId}" name="#{name}" *{attrs} type="#{inputType}" :isReq:required="" value="#{showVal val}"> |] addScript' urlJqueryJs addScript' urlJqueryUiJs@@ -98,13 +108,13 @@ ] jqueryAutocompleteField :: (RenderMessage site FormMessage, YesodJquery site)- => Route site -> Field (HandlerT site IO) Text+ => Route site -> Field (HandlerFor site) Text jqueryAutocompleteField = jqueryAutocompleteField' 2 jqueryAutocompleteField' :: (RenderMessage site FormMessage, YesodJquery site) => Int -- ^ autocomplete minimum length -> Route site- -> Field (HandlerT site IO) Text+ -> Field (HandlerFor site) Text jqueryAutocompleteField' minLen src = Field { fieldParse = parseHelper $ Right , fieldView = \theId name attrs val isReq -> do
Yesod/Form/MassInput.hs view
@@ -1,8 +1,11 @@-{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE CPP#-}+{-# LANGUAGE TypeOperators #-}++-- | A module providing a means of creating multiple input forms, such as a+-- list of 0 or more recipients. module Yesod.Form.MassInput ( inputList , massDivs@@ -40,11 +43,19 @@ IntCons _ is' -> put is' >> newFormIdent >> return () up $ i - 1 -inputList :: (m ~ HandlerT site IO, xml ~ WidgetT site IO (), RenderMessage site FormMessage)+-- | Generate a form that accepts 0 or more values from the user, allowing the+-- user to specify that a new row is necessary.+inputList :: (xml ~ WidgetFor site (), RenderMessage site FormMessage) => Html+ -- ^ label for the form -> ([[FieldView site]] -> xml)- -> (Maybe a -> AForm (HandlerT site IO) a)- -> (Maybe [a] -> AForm (HandlerT site IO) [a])+ -- ^ how to display the rows, usually either 'massDivs' or 'massTable'+ -> (Maybe a -> AForm (HandlerFor site) a)+ -- ^ display a single row of the form, where @Maybe a@ gives the+ -- previously submitted value+ -> Maybe [a]+ -- ^ default initial values for the form+ -> AForm (HandlerFor site) [a] inputList label fixXml single mdef = formToAForm $ do theId <- lift newIdent down 1@@ -84,9 +95,9 @@ , fvRequired = False }]) -withDelete :: (xml ~ WidgetT site IO (), RenderMessage site FormMessage)- => AForm (HandlerT site IO) a- -> MForm (HandlerT site IO) (Either xml (FormResult a, [FieldView site]))+withDelete :: (xml ~ WidgetFor site (), RenderMessage site FormMessage)+ => AForm (HandlerFor site) a+ -> MForm (HandlerFor site) (Either xml (FormResult a, [FieldView site])) withDelete af = do down 1 deleteName <- newFormIdent@@ -109,18 +120,17 @@ up 1 return res -fixme :: (xml ~ WidgetT site IO ())- => [Either xml (FormResult a, [FieldView site])]+fixme :: [Either xml (FormResult a, [FieldView site])] -> (FormResult [a], [xml], [[FieldView site]]) fixme eithers = (res, xmls, map snd rest) where (xmls, rest) = partitionEithers eithers- res = sequenceA $ map fst rest+ res = Data.Traversable.sequenceA $ map fst rest massDivs, massTable :: [[FieldView site]]- -> WidgetT site IO ()+ -> WidgetFor site () massDivs viewss = [whamlet| $newline never $forall views <- viewss
Yesod/Form/Nic.hs view
@@ -1,9 +1,17 @@-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE CPP #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+ -- | Provide the user with a rich text editor.+--+-- According to NIC editor homepage it is not actively maintained since June+-- 2012. There is another better alternative — open sourced Summernote editor+-- released under MIT licence. You can use Summernote in your Yesod forms via+-- separately distributed+-- <http://hackage.haskell.org/package/yesod-form-richtext yesod-form-richtext>+-- package. module Yesod.Form.Nic ( YesodNic (..) , nicHtmlField@@ -12,8 +20,7 @@ import Yesod.Core import Yesod.Form import Text.HTML.SanitizeXSS (sanitizeBalance)-import Text.Hamlet (shamlet)-import Text.Julius (julius, rawJS)+import Text.Julius (rawJS) import Text.Blaze.Html.Renderer.String (renderHtml) import Data.Text (Text, pack) import Data.Maybe (listToMaybe)@@ -23,7 +30,7 @@ urlNicEdit :: a -> Either (Route a) Text urlNicEdit _ = Right "http://js.nicedit.com/nicEdit-latest.js" -nicHtmlField :: YesodNic site => Field (HandlerT site IO) Html+nicHtmlField :: YesodNic site => Field (HandlerFor site) Html nicHtmlField = Field { fieldParse = \e _ -> return . Right . fmap (preEscapedToMarkup . sanitizeBalance) . listToMaybe $ e , fieldView = \theId name attrs val _isReq -> do
+ Yesod/Form/Option.hs view
@@ -0,0 +1,96 @@+{-# LANGUAGE FlexibleContexts #-}++module Yesod.Form.Option where++import Yesod.Core+import Yesod.Form.Fields++-- | Creates an `OptionList` from a `List`, using the `PathPiece` instance for+-- the external value and a custom function for the user-facing value.+--+-- @since 1.7.7+--+-- PathPiece instances should provide suitable external values, since path+-- pieces serve to be exposed through URLs or HTML anyway. Show/Read instances+-- are avoided here since they could leak internal representations to forms,+-- query params, javascript etc.+--+-- === __Example usage__+--+-- > data UserRole = URSalesTeam | URSalesHead | URTechTeam | URTechHead+-- >+-- > instance PathPiece UserDepartment where+-- > toPathPiece = \case+-- > URSalesTeam -> "sales-team"+-- > URSalesHead -> "sales-head"+-- > URTechTeam -> "tech-team"+-- > URTechHead -> "tech-head"+-- > fromPathPiece = \case+-- > "sales-team" -> Just URSalesTeam+-- > "sales-head" -> Just URSalesHead+-- > "tech-team" -> Just URTechTeam+-- > "tech-head" -> Just URTechHead+-- > _ -> Nothing+-- >+-- > userRoleOptions ::+-- > (MonadHandler m, RenderMessage (HandlerSite m) msg) => m (OptionList UserRole)+-- > userRoleOptions = optionsFromList' userRoles toMsg+-- > where+-- > userRoles = [URSalesTeam, URSalesHead, URTechTeam, URTechHead]+-- > toMsg :: UserRole -> Text+-- > toMsg = \case+-- > URSalesTeam -> "Sales Team"+-- > URSalesHead -> "Head of Sales Team"+-- > URTechTeam -> "Tech Team"+-- > URTechHead -> "Head of Tech Team"+--+-- userRoleOptions, will produce an OptionList with the following attributes:+--+-- > +----------------+----------------+--------------------++-- > | Internal Value | External Value | User-facing Value |+-- > +----------------+----------------+--------------------++-- > | URSalesTeam | sales-team | Sales Team |+-- > +----------------+----------------+--------------------++-- > | URSalesHead | sales-head | Head of Sales Team |+-- > +----------------+----------------+--------------------++-- > | URTechTeam | tech-team | Tech Team |+-- > +----------------+----------------+--------------------++-- > | URTechHead | tech-head | Head of Tech Team |+-- > +----------------+----------------+--------------------++--+-- Note that the type constraint allows localizable messages in place of toMsg (see+-- https://en.wikipedia.org/wiki/Yesod_(web_framework)#Localizable_messages).++optionsFromList' ::+ MonadHandler m+ => RenderMessage (HandlerSite m) msg+ => PathPiece a+ => [a]+ -> (a -> msg)+ -> m (OptionList a)+optionsFromList' lst toDisplay = do+ mr <- getMessageRender+ pure $ mkOptionList $ flip map lst $ \v -> Option+ { optionDisplay = mr $ toDisplay v+ , optionInternalValue = v+ , optionExternalValue = toPathPiece v+ }++-- | Creates an `OptionList` from an `Enum`.+--+-- @since 1.7.7+--+-- optionsEnum' == optionsFromList' [minBound..maxBound]+--+-- Creates an `OptionList` containing every constructor of `a`, so that these+-- constructors do not need to be typed out. Bounded and Enum instances must+-- exist for `a` to use this.+optionsEnum' ::+ MonadHandler m+ => RenderMessage (HandlerSite m) msg+ => PathPiece a+ => Enum a+ => Bounded a+ => (a -> msg)+ -> m (OptionList a)+optionsEnum' = optionsFromList' [minBound..maxBound]
Yesod/Form/Types.hs view
@@ -1,8 +1,9 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE CPP #-}+{-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeFamilies #-}+ module Yesod.Form.Types ( -- * Helpers Enctype (..)@@ -12,6 +13,7 @@ , FileEnv , Ints (..) -- * Form+ , WForm , MForm , AForm (..) -- * Build forms@@ -22,28 +24,32 @@ ) where import Control.Monad.Trans.RWS (RWST)+import Control.Monad.Trans.Writer (WriterT) import Data.Text (Text)-import Data.Monoid (Monoid (..))-import Text.Blaze (Markup, ToMarkup (toMarkup))+import Text.Blaze (Markup, ToMarkup (toMarkup), ToValue (toValue)) #define Html Markup #define ToHtml ToMarkup #define toHtml toMarkup-import Control.Applicative ((<$>), Applicative (..))+import Control.Applicative (Alternative (..)) import Control.Monad (liftM) import Control.Monad.Trans.Class import Data.String (IsString (..)) import Yesod.Core import qualified Data.Map as Map+import Data.Traversable+import Data.Foldable -- | A form can produce three different results: there was no data available, -- the data was invalid, or there was a successful parse. -- -- The 'Applicative' instance will concatenate the failure messages in two -- 'FormResult's.+-- The 'Alternative' instance will choose 'FormFailure' before 'FormSuccess',+-- and 'FormMissing' last of all. data FormResult a = FormMissing | FormFailure [Text] | FormSuccess a- deriving Show+ deriving (Show, Eq) instance Functor FormResult where fmap _ FormMissing = FormMissing fmap _ (FormFailure errs) = FormFailure errs@@ -57,8 +63,34 @@ _ <*> _ = FormMissing instance Monoid m => Monoid (FormResult m) where mempty = pure mempty- mappend x y = mappend <$> x <*> y+ mappend = (<>)+instance Semigroup m => Semigroup (FormResult m) where+ x <> y = (<>) <$> x <*> y +-- | @since 1.4.5+instance Data.Foldable.Foldable FormResult where+ foldMap f r = case r of+ FormSuccess a -> f a+ FormFailure _errs -> mempty+ FormMissing -> mempty++-- | @since 1.4.5+instance Data.Traversable.Traversable FormResult where+ traverse f r = case r of+ FormSuccess a -> fmap FormSuccess (f a)+ FormFailure errs -> pure (FormFailure errs)+ FormMissing -> pure FormMissing++-- | @since 1.4.15+instance Alternative FormResult where+ empty = FormMissing++ FormFailure e <|> _ = FormFailure e+ _ <|> FormFailure e = FormFailure e+ FormSuccess s <|> FormSuccess _ = FormSuccess s+ FormMissing <|> result = result+ result <|> FormMissing = result+ -- | The encoding type required by a form. The 'ToHtml' instance produces values -- that can be inserted directly into HTML. data Enctype = UrlEncoded | Multipart@@ -66,10 +98,14 @@ instance ToHtml Enctype where toHtml UrlEncoded = "application/x-www-form-urlencoded" toHtml Multipart = "multipart/form-data"+instance ToValue Enctype where+ toValue UrlEncoded = "application/x-www-form-urlencoded"+ toValue Multipart = "multipart/form-data" instance Monoid Enctype where mempty = UrlEncoded- mappend UrlEncoded UrlEncoded = UrlEncoded- mappend _ _ = Multipart+instance Semigroup Enctype where+ UrlEncoded <> UrlEncoded = UrlEncoded+ _ <> _ = Multipart data Ints = IntCons Int Ints | IntSingle Int instance Show Ints where@@ -79,6 +115,29 @@ type Env = Map.Map Text [Text] type FileEnv = Map.Map Text [FileInfo] +-- | 'MForm' variant stacking a 'WriterT'. The following code example using a+-- monadic form 'MForm':+--+-- > formToAForm $ do+-- > (field1F, field1V) <- mreq textField MsgField1 Nothing+-- > (field2F, field2V) <- mreq (checkWith field1F textField) MsgField2 Nothing+-- > (field3F, field3V) <- mreq (checkWith field1F textField) MsgField3 Nothing+-- > return+-- > ( MyForm <$> field1F <*> field2F <*> field3F+-- > , [field1V, field2V, field3V]+-- > )+--+-- Could be rewritten as follows using 'WForm':+--+-- > wFormToAForm $ do+-- > field1F <- wreq textField MsgField1 Nothing+-- > field2F <- wreq (checkWith field1F textField) MsgField2 Nothing+-- > field3F <- wreq (checkWith field1F textField) MsgField3 Nothing+-- > return $ MyForm <$> field1F <*> field2F <*> field3F+--+-- @since 1.4.14+type WForm m a = MForm (WriterT [FieldView (HandlerSite m)] m) a+ type MForm m a = RWST (Maybe (Env, FileEnv), HandlerSite m, [Lang]) Enctype@@ -103,9 +162,23 @@ (a, b, ints', c) <- f mr env ints (x, y, ints'', z) <- g mr env ints' return (a <*> x, b . y, ints'', c `mappend` z)++#if MIN_VERSION_transformers(0,6,0)+instance Monad m => Monad (AForm m) where+ (AForm f) >>= k = AForm $ \mr env ints -> do+ (a, b, ints', c) <- f mr env ints+ case a of+ FormSuccess r -> do+ (x, y, ints'', z) <- unAForm (k r) mr env ints'+ return (x, b . y, ints'', c `mappend` z)+ FormFailure err -> pure (FormFailure err, b, ints', c)+ FormMissing -> pure (FormMissing, b, ints', c)+#endif instance (Monad m, Monoid a) => Monoid (AForm m a) where mempty = pure mempty- mappend a b = mappend <$> a <*> b+ mappend = (<>)+instance (Monad m, Semigroup a) => Semigroup (AForm m a) where+ a <> b = (<>) <$> a <*> b instance MonadTrans AForm where lift f = AForm $ \_ _ ints -> do@@ -127,7 +200,7 @@ { fvLabel :: Html , fvTooltip :: Maybe Html , fvId :: Text- , fvInput :: WidgetT site IO ()+ , fvInput :: WidgetFor site () , fvErrors :: Maybe Html , fvRequired :: Bool }@@ -138,7 +211,7 @@ -> [(Text, Text)] -- ^ Attributes -> Either Text a -- ^ Either (invalid text) or (legitimate result) -> Bool -- ^ Required?- -> WidgetT (HandlerSite m) IO ()+ -> WidgetFor (HandlerSite m) () data Field m a = Field { fieldParse :: [Text] -> [FileInfo] -> m (Either (SomeMessage (HandlerSite m)) (Maybe a))@@ -164,4 +237,6 @@ | MsgBoolYes | MsgBoolNo | MsgDelete+ | MsgInvalidHexColorFormat Text+ | MsgInvalidDatetimeFormat Text deriving (Show, Eq, Read)
yesod-form.cabal view
@@ -1,5 +1,6 @@+cabal-version: >= 1.10 name: yesod-form-version: 1.3.15.1+version: 1.7.9.3 license: MIT license-file: LICENSE author: Michael Snoyman <michael@snoyman.com>@@ -7,48 +8,45 @@ synopsis: Form handling support for Yesod Web Framework category: Web, Yesod stability: Stable-cabal-version: >= 1.8 build-type: Simple homepage: http://www.yesodweb.com/-description: Form handling support for Yesod Web Framework+description: API docs and the README are available at <http://www.stackage.org/package/yesod-form>. Third-party packages which you can find useful: <http://hackage.haskell.org/package/yesod-form-richtext yesod-form-richtext> - richtext form fields (currently it provides only Summernote support).+extra-source-files: ChangeLog.md+ README.md flag network-uri description: Get Network.URI from the network-uri package default: True library- build-depends: base >= 4 && < 5- , yesod-core >= 1.2 && < 1.3- , yesod-persistent >= 1.2 && < 1.3- , time >= 1.1.4- , hamlet >= 1.1.8- , shakespeare- , shakespeare-css >= 1.0- , shakespeare-js >= 1.0.2- , persistent >= 1.2 && < 1.4- , template-haskell- , transformers >= 0.2.2- , data-default- , xss-sanitize >= 0.3.0.1+ default-language: Haskell2010+ build-depends: base >= 4.11 && < 5+ , aeson+ , attoparsec >= 0.10 , blaze-builder >= 0.2.1.4- , email-validate >= 1.0- , bytestring >= 0.9.1.4- , text >= 0.9- , wai >= 1.3- , containers >= 0.2 , blaze-html >= 0.5 , blaze-markup >= 0.5.1- , attoparsec >= 0.10 , byteable- , aeson+ , bytestring >= 0.9.1.4+ , containers >= 0.2+ , data-default+ , email-validate >= 1.0+ , persistent , resourcet+ , shakespeare >= 2.0+ , text >= 0.9+ , time >= 1.1.4+ , transformers >= 0.2.2+ , wai >= 1.3+ , xss-sanitize >= 0.3.0.1+ , yesod-core >= 1.6 && < 1.8+ , yesod-persistent >= 1.6 && < 1.7 if flag(network-uri) build-depends: network-uri >= 2.6- else- build-depends: network < 2.6 exposed-modules: Yesod.Form+ Yesod.Form.Option Yesod.Form.Types Yesod.Form.Functions Yesod.Form.Bootstrap3@@ -63,13 +61,19 @@ Yesod.Form.I18n.German Yesod.Form.I18n.French Yesod.Form.I18n.Norwegian- Yesod.Form.I18n.Japanese + Yesod.Form.I18n.Japanese Yesod.Form.I18n.Czech Yesod.Form.I18n.Russian+ Yesod.Form.I18n.Dutch+ Yesod.Form.I18n.Spanish+ Yesod.Form.I18n.Chinese+ Yesod.Form.I18n.Korean+ Yesod.Form.I18n.Romanian -- FIXME Yesod.Helpers.Crud ghc-options: -Wall test-suite test+ default-language: Haskell2010 type: exitcode-stdio-1.0 main-is: main.hs hs-source-dirs: test