Add 'yesod-form/' from commit '53b7c3b81015e7f244c6b8961ca2f07979f932e2'
git-subtree-dir: yesod-form git-subtree-mainline:41faf62094git-subtree-split:53b7c3b810
This commit is contained in:
commit
6f5459f70f
25
yesod-form/LICENSE
Normal file
25
yesod-form/LICENSE
Normal file
@ -0,0 +1,25 @@
|
||||
The following license covers this documentation, and the source code, except
|
||||
where otherwise indicated.
|
||||
|
||||
Copyright 2010, Michael Snoyman. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
||||
EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
|
||||
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
||||
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
0
yesod-form/README
Normal file
0
yesod-form/README
Normal file
7
yesod-form/Setup.lhs
Executable file
7
yesod-form/Setup.lhs
Executable file
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env runhaskell
|
||||
|
||||
> module Main where
|
||||
> import Distribution.Simple
|
||||
|
||||
> main :: IO ()
|
||||
> main = defaultMain
|
||||
14
yesod-form/Yesod/Form.hs
Normal file
14
yesod-form/Yesod/Form.hs
Normal file
@ -0,0 +1,14 @@
|
||||
-- | Parse forms (and query strings).
|
||||
module Yesod.Form
|
||||
( module Yesod.Form.Types
|
||||
, module Yesod.Form.Functions
|
||||
, module Yesod.Form.Fields
|
||||
, module Yesod.Form.Class
|
||||
, module Yesod.Form.Input
|
||||
) where
|
||||
|
||||
import Yesod.Form.Types
|
||||
import Yesod.Form.Functions
|
||||
import Yesod.Form.Fields
|
||||
import Yesod.Form.Class
|
||||
import Yesod.Form.Input
|
||||
77
yesod-form/Yesod/Form/Class.hs
Normal file
77
yesod-form/Yesod/Form/Class.hs
Normal file
@ -0,0 +1,77 @@
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE TypeSynonymInstances #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
module Yesod.Form.Class
|
||||
( ToForm (..)
|
||||
, ToField (..)
|
||||
) where
|
||||
|
||||
import Text.Hamlet
|
||||
import Yesod.Widget (GGWidget)
|
||||
import Yesod.Form.Fields
|
||||
import Yesod.Form.Types
|
||||
import Yesod.Form.Functions (areq, aopt)
|
||||
import Data.Int (Int64)
|
||||
import Data.Time (Day, TimeOfDay)
|
||||
import Data.Text (Text)
|
||||
import Yesod.Handler (GGHandler)
|
||||
import Yesod.Message (RenderMessage)
|
||||
|
||||
class ToForm a master monad where
|
||||
toForm :: AForm ([FieldView (GGWidget master monad ())] -> [FieldView (GGWidget master monad ())]) master monad a
|
||||
|
||||
class ToField a master monad where
|
||||
toField :: RenderMessage master msg => FieldSettings msg -> Maybe a -> AForm ([FieldView (GGWidget master monad ())] -> [FieldView (GGWidget master monad ())]) master monad a
|
||||
|
||||
{- FIXME
|
||||
instance ToFormField String y where
|
||||
toFormField = stringField
|
||||
instance ToFormField (Maybe String) y where
|
||||
toFormField = maybeStringField
|
||||
-}
|
||||
|
||||
instance (Monad m, RenderMessage master FormMessage) => ToField Text master (GGHandler sub master m) where
|
||||
toField = areq textField
|
||||
instance (Monad m, RenderMessage master FormMessage) => ToField (Maybe Text) master (GGHandler sub master m) where
|
||||
toField = aopt textField
|
||||
|
||||
instance (Monad m, RenderMessage master FormMessage) => ToField Int master (GGHandler sub master m) where
|
||||
toField = areq intField
|
||||
instance (Monad m, RenderMessage master FormMessage) => ToField (Maybe Int) master (GGHandler sub master m) where
|
||||
toField = aopt intField
|
||||
|
||||
instance (Monad m, RenderMessage master FormMessage) => ToField Int64 master (GGHandler sub master m) where
|
||||
toField = areq intField
|
||||
instance (Monad m, RenderMessage master FormMessage) => ToField (Maybe Int64) master (GGHandler sub master m) where
|
||||
toField = aopt intField
|
||||
|
||||
instance (Monad m, RenderMessage master FormMessage) => ToField Double master (GGHandler sub master m) where
|
||||
toField = areq doubleField
|
||||
instance (Monad m, RenderMessage master FormMessage) => ToField (Maybe Double) master (GGHandler sub master m) where
|
||||
toField = aopt doubleField
|
||||
|
||||
instance (Monad m, RenderMessage master FormMessage) => ToField Day master (GGHandler sub master m) where
|
||||
toField = areq dayField
|
||||
instance (Monad m, RenderMessage master FormMessage) => ToField (Maybe Day) master (GGHandler sub master m) where
|
||||
toField = aopt dayField
|
||||
|
||||
instance (Monad m, RenderMessage master FormMessage) => ToField TimeOfDay master (GGHandler sub master m) where
|
||||
toField = areq timeField
|
||||
instance (Monad m, RenderMessage master FormMessage) => ToField (Maybe TimeOfDay) master (GGHandler sub master m) where
|
||||
toField = aopt timeField
|
||||
|
||||
instance (Monad m, RenderMessage master FormMessage) => ToField Html master (GGHandler sub master m) where
|
||||
toField = areq htmlField
|
||||
instance (Monad m, RenderMessage master FormMessage) => ToField (Maybe Html) master (GGHandler sub master m) where
|
||||
toField = aopt htmlField
|
||||
|
||||
instance (Monad m, RenderMessage master FormMessage) => ToField Textarea master (GGHandler sub master m) where
|
||||
toField = areq textareaField
|
||||
instance (Monad m, RenderMessage master FormMessage) => ToField (Maybe Textarea) master (GGHandler sub master m) where
|
||||
toField = aopt textareaField
|
||||
|
||||
{- FIXME
|
||||
instance ToFormField Bool y where
|
||||
toFormField = boolField
|
||||
-}
|
||||
421
yesod-form/Yesod/Form/Fields.hs
Normal file
421
yesod-form/Yesod/Form/Fields.hs
Normal file
@ -0,0 +1,421 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
module Yesod.Form.Fields
|
||||
( FormMessage (..)
|
||||
, defaultFormMessage
|
||||
, textField
|
||||
, passwordField
|
||||
, textareaField
|
||||
, hiddenField
|
||||
, intField
|
||||
, dayField
|
||||
, timeField
|
||||
, htmlField
|
||||
, emailField
|
||||
, searchField
|
||||
, selectField
|
||||
, multiSelectField
|
||||
, AutoFocus
|
||||
, urlField
|
||||
, doubleField
|
||||
, parseDate
|
||||
, parseTime
|
||||
, Textarea (..)
|
||||
, radioField
|
||||
, boolField
|
||||
) where
|
||||
|
||||
import Yesod.Form.Types
|
||||
import Yesod.Widget
|
||||
import Yesod.Message (RenderMessage)
|
||||
import Yesod.Handler (GGHandler)
|
||||
import Text.Hamlet
|
||||
import Text.Blaze (ToHtml (..), preEscapedString, unsafeByteString)
|
||||
import Text.Cassius
|
||||
import Data.Time (Day, TimeOfDay(..))
|
||||
import qualified Text.Email.Validate as Email
|
||||
import Network.URI (parseURI)
|
||||
import Database.Persist (PersistField)
|
||||
import Text.HTML.SanitizeXSS (sanitizeBalance)
|
||||
import Control.Monad (when, unless)
|
||||
import Data.List (intersect, nub)
|
||||
import Data.Either (rights)
|
||||
import Data.Maybe (catMaybes)
|
||||
|
||||
import qualified Blaze.ByteString.Builder.Html.Utf8 as B
|
||||
import Blaze.ByteString.Builder (writeByteString, toLazyByteString)
|
||||
import Blaze.ByteString.Builder.Internal.Write (fromWriteList)
|
||||
|
||||
import Text.Blaze.Renderer.String (renderHtml)
|
||||
import qualified Data.ByteString as S
|
||||
import qualified Data.ByteString.Lazy as L
|
||||
import Data.Text (Text, unpack, pack)
|
||||
import qualified Data.Text.Read
|
||||
import Data.Monoid (mappend)
|
||||
import Text.Hamlet (html)
|
||||
|
||||
#if __GLASGOW_HASKELL__ >= 700
|
||||
#define WHAMLET whamlet
|
||||
#define HAMLET hamlet
|
||||
#define CASSIUS cassius
|
||||
#define JULIUS julius
|
||||
#define HTML html
|
||||
#else
|
||||
#define WHAMLET $whamlet
|
||||
#define HAMLET $hamlet
|
||||
#define CASSIUS $cassius
|
||||
#define JULIUS $julius
|
||||
#define HTML $html
|
||||
#endif
|
||||
|
||||
data FormMessage = MsgInvalidInteger Text
|
||||
| MsgInvalidNumber Text
|
||||
| MsgInvalidEntry Text
|
||||
| MsgInvalidUrl Text
|
||||
| MsgInvalidEmail Text
|
||||
| MsgInvalidTimeFormat
|
||||
| MsgInvalidHour Text
|
||||
| MsgInvalidMinute Text
|
||||
| MsgInvalidSecond Text
|
||||
| MsgInvalidDay
|
||||
| MsgCsrfWarning
|
||||
| MsgValueRequired
|
||||
| MsgInputNotFound Text
|
||||
| MsgSelectNone
|
||||
| MsgInvalidBool Text
|
||||
| MsgBoolYes
|
||||
| MsgBoolNo
|
||||
|
||||
defaultFormMessage :: FormMessage -> Text
|
||||
defaultFormMessage (MsgInvalidInteger t) = "Invalid integer: " `mappend` t
|
||||
defaultFormMessage (MsgInvalidNumber t) = "Invalid number: " `mappend` t
|
||||
defaultFormMessage (MsgInvalidEntry t) = "Invalid entry: " `mappend` t
|
||||
defaultFormMessage MsgInvalidTimeFormat = "Invalid time, must be in HH:MM[:SS] format"
|
||||
defaultFormMessage MsgInvalidDay = "Invalid day, must be in YYYY-MM-DD format"
|
||||
defaultFormMessage (MsgInvalidUrl t) = "Invalid URL: " `mappend` t
|
||||
defaultFormMessage (MsgInvalidEmail t) = "Invalid e-mail address: " `mappend` t
|
||||
defaultFormMessage (MsgInvalidHour t) = "Invalid hour: " `mappend` t
|
||||
defaultFormMessage (MsgInvalidMinute t) = "Invalid minute: " `mappend` t
|
||||
defaultFormMessage (MsgInvalidSecond t) = "Invalid second: " `mappend` t
|
||||
defaultFormMessage MsgCsrfWarning = "As a protection against cross-site request forgery attacks, please confirm your form submission."
|
||||
defaultFormMessage MsgValueRequired = "Value is required"
|
||||
defaultFormMessage (MsgInputNotFound t) = "Input not found: " `mappend` t
|
||||
defaultFormMessage MsgSelectNone = "<None>"
|
||||
defaultFormMessage (MsgInvalidBool t) = "Invalid boolean: " `mappend` t
|
||||
defaultFormMessage MsgBoolYes = "Yes"
|
||||
defaultFormMessage MsgBoolNo = "No"
|
||||
|
||||
blank :: (Text -> Either msg a) -> [Text] -> Either msg (Maybe a)
|
||||
blank _ [] = Right Nothing
|
||||
blank _ ("":_) = Right Nothing
|
||||
blank f (x:_) = either Left (Right . Just) $ f x
|
||||
|
||||
|
||||
|
||||
intField :: (Monad monad, Integral i) => Field (GGWidget master monad ()) FormMessage i
|
||||
intField = Field
|
||||
{ fieldParse = blank $ \s ->
|
||||
case Data.Text.Read.signed Data.Text.Read.decimal s of
|
||||
Right (a, "") -> Right a
|
||||
_ -> Left $ MsgInvalidInteger s
|
||||
|
||||
, fieldView = \theId name val isReq -> addHamlet
|
||||
[HAMLET|\
|
||||
<input id="#{theId}" name="#{name}" type="number" :isReq:required="" value="#{showVal val}">
|
||||
|]
|
||||
}
|
||||
where
|
||||
showVal = either id (pack . showI)
|
||||
showI x = show (fromIntegral x :: Integer)
|
||||
|
||||
doubleField :: Monad monad => Field (GGWidget master monad ()) FormMessage Double
|
||||
doubleField = Field
|
||||
{ fieldParse = blank $ \s ->
|
||||
case Data.Text.Read.double s of
|
||||
Right (a, "") -> Right a
|
||||
_ -> Left $ MsgInvalidNumber s
|
||||
|
||||
, fieldView = \theId name val isReq -> addHamlet
|
||||
[HAMLET|\
|
||||
<input id="#{theId}" name="#{name}" type="text" :isReq:required="" value="#{showVal val}">
|
||||
|]
|
||||
}
|
||||
where showVal = either id (pack . show)
|
||||
|
||||
dayField :: Monad monad => Field (GGWidget master monad ()) FormMessage Day
|
||||
dayField = Field
|
||||
{ fieldParse = blank $ parseDate . unpack
|
||||
, fieldView = \theId name val isReq -> addHamlet
|
||||
[HAMLET|\
|
||||
<input id="#{theId}" name="#{name}" type="date" :isReq:required="" value="#{showVal val}">
|
||||
|]
|
||||
}
|
||||
where showVal = either id (pack . show)
|
||||
|
||||
timeField :: Monad monad => Field (GGWidget master monad ()) FormMessage TimeOfDay
|
||||
timeField = Field
|
||||
{ fieldParse = blank $ parseTime . unpack
|
||||
, fieldView = \theId name val isReq -> addHamlet
|
||||
[HAMLET|\
|
||||
<input id="#{theId}" name="#{name}" :isReq:required="" value="#{showVal val}">
|
||||
|]
|
||||
}
|
||||
where
|
||||
showVal = either id (pack . show . roundFullSeconds)
|
||||
roundFullSeconds tod =
|
||||
TimeOfDay (todHour tod) (todMin tod) fullSec
|
||||
where
|
||||
fullSec = fromInteger $ floor $ todSec tod
|
||||
|
||||
htmlField :: Monad monad => Field (GGWidget master monad ()) FormMessage Html
|
||||
htmlField = Field
|
||||
{ fieldParse = blank $ Right . preEscapedString . sanitizeBalance . unpack -- FIXME make changes to xss-sanitize
|
||||
, fieldView = \theId name val _isReq -> addHamlet
|
||||
[HAMLET|\
|
||||
<textarea id="#{theId}" name="#{name}" .html>#{showVal val}
|
||||
|]
|
||||
}
|
||||
where showVal = either id (pack . renderHtml)
|
||||
|
||||
-- | A newtype wrapper around a 'String' that converts newlines to HTML
|
||||
-- br-tags.
|
||||
newtype Textarea = Textarea { unTextarea :: Text }
|
||||
deriving (Show, Read, Eq, PersistField)
|
||||
instance ToHtml Textarea where
|
||||
toHtml =
|
||||
unsafeByteString
|
||||
. S.concat
|
||||
. L.toChunks
|
||||
. toLazyByteString
|
||||
. fromWriteList writeHtmlEscapedChar
|
||||
. unpack
|
||||
. unTextarea
|
||||
where
|
||||
-- Taken from blaze-builder and modified with newline handling.
|
||||
writeHtmlEscapedChar '\n' = writeByteString "<br>"
|
||||
writeHtmlEscapedChar c = B.writeHtmlEscapedChar c
|
||||
|
||||
textareaField :: Monad monad => Field (GGWidget master monad ()) FormMessage Textarea
|
||||
textareaField = Field
|
||||
{ fieldParse = blank $ Right . Textarea
|
||||
, fieldView = \theId name val _isReq -> addHamlet
|
||||
[HAMLET|\
|
||||
<textarea id="#{theId}" name="#{name}">#{either id unTextarea val}
|
||||
|]
|
||||
}
|
||||
|
||||
hiddenField :: Monad monad => Field (GGWidget master monad ()) FormMessage Text
|
||||
hiddenField = Field
|
||||
{ fieldParse = blank $ Right
|
||||
, fieldView = \theId name val _isReq -> addHamlet
|
||||
[HAMLET|\
|
||||
<input type="hidden" id="#{theId}" name="#{name}" value="#{either id id val}">
|
||||
|]
|
||||
}
|
||||
|
||||
textField :: Monad monad => Field (GGWidget master monad ()) FormMessage Text
|
||||
textField = Field
|
||||
{ fieldParse = blank $ Right
|
||||
, fieldView = \theId name val isReq ->
|
||||
[WHAMLET|
|
||||
<input id="#{theId}" name="#{name}" type="text" :isReq:required value="#{either id id val}">
|
||||
|]
|
||||
}
|
||||
|
||||
passwordField :: Monad monad => Field (GGWidget master monad ()) FormMessage Text
|
||||
passwordField = Field
|
||||
{ fieldParse = blank $ Right
|
||||
, fieldView = \theId name val isReq -> addHamlet
|
||||
[HAMLET|\
|
||||
<input id="#{theId}" name="#{name}" type="password" :isReq:required="" value="#{either id id val}">
|
||||
|]
|
||||
}
|
||||
|
||||
readMay :: Read a => String -> Maybe a
|
||||
readMay s = case reads s of
|
||||
(x, _):_ -> Just x
|
||||
[] -> Nothing
|
||||
|
||||
parseDate :: String -> Either FormMessage Day
|
||||
parseDate = maybe (Left MsgInvalidDay) Right
|
||||
. readMay . replace '/' '-'
|
||||
|
||||
-- | Replaces all instances of a value in a list by another value.
|
||||
-- from http://hackage.haskell.org/packages/archive/cgi/3001.1.7.1/doc/html/src/Network-CGI-Protocol.html#replace
|
||||
replace :: Eq a => a -> a -> [a] -> [a]
|
||||
replace x y = map (\z -> if z == x then y else z)
|
||||
|
||||
parseTime :: String -> Either FormMessage TimeOfDay
|
||||
parseTime (h2:':':m1:m2:[]) = parseTimeHelper ('0', h2, m1, m2, '0', '0')
|
||||
parseTime (h1:h2:':':m1:m2:[]) = parseTimeHelper (h1, h2, m1, m2, '0', '0')
|
||||
parseTime (h1:h2:':':m1:m2:' ':'A':'M':[]) =
|
||||
parseTimeHelper (h1, h2, m1, m2, '0', '0')
|
||||
parseTime (h1:h2:':':m1:m2:' ':'P':'M':[]) =
|
||||
let [h1', h2'] = show $ (read [h1, h2] :: Int) + 12
|
||||
in parseTimeHelper (h1', h2', m1, m2, '0', '0')
|
||||
parseTime (h1:h2:':':m1:m2:':':s1:s2:[]) =
|
||||
parseTimeHelper (h1, h2, m1, m2, s1, s2)
|
||||
parseTime _ = Left MsgInvalidTimeFormat
|
||||
|
||||
parseTimeHelper :: (Char, Char, Char, Char, Char, Char)
|
||||
-> Either FormMessage TimeOfDay
|
||||
parseTimeHelper (h1, h2, m1, m2, s1, s2)
|
||||
| h < 0 || h > 23 = Left $ MsgInvalidHour $ pack [h1, h2]
|
||||
| m < 0 || m > 59 = Left $ MsgInvalidMinute $ pack [m1, m2]
|
||||
| s < 0 || s > 59 = Left $ MsgInvalidSecond $ pack [s1, s2]
|
||||
| otherwise = Right $ TimeOfDay h m s
|
||||
where
|
||||
h = read [h1, h2] -- FIXME isn't this a really bad idea?
|
||||
m = read [m1, m2]
|
||||
s = fromInteger $ read [s1, s2]
|
||||
|
||||
emailField :: Monad monad => Field (GGWidget master monad ()) FormMessage Text
|
||||
emailField = Field
|
||||
{ fieldParse = blank $
|
||||
\s -> if Email.isValid (unpack s)
|
||||
then Right s
|
||||
else Left $ MsgInvalidEmail s
|
||||
, fieldView = \theId name val isReq -> addHamlet
|
||||
[HAMLET|\
|
||||
<input id="#{theId}" name="#{name}" type="email" :isReq:required="" value="#{either id id val}">
|
||||
|]
|
||||
}
|
||||
|
||||
type AutoFocus = Bool
|
||||
searchField :: Monad monad => AutoFocus -> Field (GGWidget master monad ()) FormMessage Text
|
||||
searchField autoFocus = Field
|
||||
{ fieldParse = blank Right
|
||||
, fieldView = \theId name val isReq -> do
|
||||
[WHAMLET|\
|
||||
<input id="#{theId}" name="#{name}" type="search" :isReq:required="" :autoFocus:autofocus="" value="#{either id id val}">
|
||||
|]
|
||||
when autoFocus $ do
|
||||
[WHAMLET|\<script>if (!('autofocus' in document.createElement('input'))) {document.getElementById('#{theId}').focus();}</script>
|
||||
|]
|
||||
addCassius [CASSIUS|
|
||||
#{theId}
|
||||
-webkit-appearance: textfield
|
||||
|]
|
||||
}
|
||||
|
||||
urlField :: Monad monad => Field (GGWidget master monad ()) FormMessage Text
|
||||
urlField = Field
|
||||
{ fieldParse = blank $ \s ->
|
||||
case parseURI $ unpack s of
|
||||
Nothing -> Left $ MsgInvalidUrl s
|
||||
Just _ -> Right s
|
||||
, fieldView = \theId name val isReq ->
|
||||
[WHAMLET|
|
||||
<input ##{theId} name=#{name} type=url :isReq:required value=#{either id id val}>
|
||||
|]
|
||||
}
|
||||
|
||||
selectField :: (Eq a, Monad monad, RenderMessage master FormMessage) => [(Text, a)] -> Field (GGWidget master (GGHandler sub master monad) ()) FormMessage a
|
||||
selectField = selectFieldHelper
|
||||
(\theId name inside -> [WHAMLET|<select ##{theId} name=#{name}>^{inside}|])
|
||||
(\_theId _name isSel -> [WHAMLET|<option value=none :isSel:selected>_{MsgSelectNone}|])
|
||||
(\_theId _name value isSel text -> [WHAMLET|<option value=#{value} :isSel:selected>#{text}|])
|
||||
|
||||
multiSelectField :: (Show a, Eq a, Monad monad, RenderMessage master FormMessage) => [(Text, a)] -> Field (GGWidget master (GGHandler sub master monad) ()) FormMessage [a]
|
||||
multiSelectField = multiSelectFieldHelper
|
||||
(\theId name inside -> [WHAMLET|<select ##{theId} multiple name=#{name}>^{inside}|])
|
||||
(\_theId _name value isSel text -> [WHAMLET|<option value=#{value} :isSel:selected>#{text}|])
|
||||
|
||||
radioField :: (Eq a, Monad monad, RenderMessage master FormMessage) => [(Text, a)] -> Field (GGWidget master (GGHandler sub master monad) ()) FormMessage a
|
||||
radioField = selectFieldHelper
|
||||
(\theId _name inside -> [WHAMLET|<div ##{theId}>^{inside}|])
|
||||
(\theId name isSel -> [WHAMLET|
|
||||
<div>
|
||||
<input id=#{theId}-none type=radio name=#{name} value=none :isSel:checked>
|
||||
<label for=#{theId}-none>_{MsgSelectNone}
|
||||
|])
|
||||
(\theId name value isSel text -> [WHAMLET|
|
||||
<div>
|
||||
<input id=#{theId}-#{value} type=radio name=#{name} value=#{value} :isSel:checked>
|
||||
<label for=#{theId}-#{value}>#{text}
|
||||
|])
|
||||
|
||||
boolField :: (Monad monad, RenderMessage master FormMessage) => Field (GGWidget master (GGHandler sub master monad) ()) FormMessage Bool
|
||||
boolField = Field
|
||||
{ fieldParse = boolParser
|
||||
, fieldView = \theId name val isReq -> [WHAMLET|
|
||||
$if not isReq
|
||||
<input id=#{theId}-none type=radio name=#{name} value=none checked>
|
||||
<label for=#{theId}-none>_{MsgSelectNone}
|
||||
|
||||
|
||||
<input id=#{theId}-yes type=radio name=#{name} value=yes :showVal id val:checked>
|
||||
<label for=#{theId}-yes>_{MsgBoolYes}
|
||||
|
||||
<input id=#{theId}-no type=radio name=#{name} value=no :showVal not val:checked>
|
||||
<label for=#{theId}-no>_{MsgBoolNo}
|
||||
|]
|
||||
}
|
||||
where
|
||||
boolParser [] = Right Nothing
|
||||
boolParser (x:_) = case x of
|
||||
"" -> Right Nothing
|
||||
"none" -> Right Nothing
|
||||
"yes" -> Right $ Just True
|
||||
"no" -> Right $ Just False
|
||||
t -> Left $ MsgInvalidBool t
|
||||
showVal = either (\_ -> False)
|
||||
|
||||
multiSelectFieldHelper :: (Show a, Eq a, Monad monad)
|
||||
=> (Text -> Text -> GGWidget master monad () -> GGWidget master monad ())
|
||||
-> (Text -> Text -> Text -> Bool -> Text -> GGWidget master monad ())
|
||||
-> [(Text, a)] -> Field (GGWidget master monad ()) FormMessage [a]
|
||||
multiSelectFieldHelper outside inside opts = Field
|
||||
{ fieldParse = selectParser
|
||||
, fieldView = \theId name vals _ ->
|
||||
outside theId name $ do
|
||||
flip mapM_ pairs $ \pair -> inside
|
||||
theId
|
||||
name
|
||||
(pack $ show $ fst pair)
|
||||
((fst pair) `elem` (either (\_ -> []) selectedVals vals)) -- We are presuming that select fields can't hold invalid values
|
||||
(fst $ snd pair)
|
||||
}
|
||||
where
|
||||
pairs = zip [1 :: Int ..] opts -- FIXME use IntMap
|
||||
rpairs = zip (map snd opts) [1 :: Int ..]
|
||||
selectedVals vals = map snd $ filter (\y -> fst y `elem` vals) rpairs
|
||||
selectParser [] = Right Nothing
|
||||
selectParser xs | not $ null (["", "none"] `intersect` xs) = Right Nothing
|
||||
| otherwise = (Right . Just . map snd . catMaybes . map (\y -> lookup y pairs) . nub . map fst . rights . map Data.Text.Read.decimal) xs
|
||||
|
||||
selectFieldHelper :: (Eq a, Monad monad)
|
||||
=> (Text -> Text -> GGWidget master monad () -> GGWidget master monad ())
|
||||
-> (Text -> Text -> Bool -> GGWidget master monad ())
|
||||
-> (Text -> Text -> Text -> Bool -> Text -> GGWidget master monad ())
|
||||
-> [(Text, a)] -> Field (GGWidget master monad ()) FormMessage a
|
||||
selectFieldHelper outside onOpt inside opts = Field
|
||||
{ fieldParse = selectParser
|
||||
, fieldView = \theId name val isReq ->
|
||||
outside theId name $ do
|
||||
unless isReq $ onOpt theId name $ not $ (render val) `elem` map (pack . show . fst) pairs
|
||||
flip mapM_ pairs $ \pair -> inside
|
||||
theId
|
||||
name
|
||||
(pack $ show $ fst pair)
|
||||
((render val) == pack (show $ fst pair))
|
||||
(fst $ snd pair)
|
||||
}
|
||||
where
|
||||
pairs = zip [1 :: Int ..] opts -- FIXME use IntMap
|
||||
rpairs = zip (map snd opts) [1 :: Int ..]
|
||||
render (Left _) = ""
|
||||
render (Right a) = maybe "" (pack . show) $ lookup a rpairs
|
||||
selectParser [] = Right Nothing
|
||||
selectParser (s:_) = case s of
|
||||
"" -> Right Nothing
|
||||
"none" -> Right Nothing
|
||||
x -> case Data.Text.Read.decimal x of
|
||||
Right (a, "") ->
|
||||
case lookup a pairs of
|
||||
Nothing -> Left $ MsgInvalidEntry x
|
||||
Just y -> Right $ Just $ snd y
|
||||
_ -> Left $ MsgInvalidNumber x
|
||||
234
yesod-form/Yesod/Form/Functions.hs
Normal file
234
yesod-form/Yesod/Form/Functions.hs
Normal file
@ -0,0 +1,234 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
module Yesod.Form.Functions
|
||||
( -- * Running in Form monad
|
||||
newFormIdent
|
||||
, askParams
|
||||
, askFiles
|
||||
-- * Applicative/Monadic conversion
|
||||
, formToAForm
|
||||
, aFormToForm
|
||||
-- * Fields to Forms
|
||||
, mreq
|
||||
, mopt
|
||||
, areq
|
||||
, aopt
|
||||
-- * Run a form
|
||||
, runFormPost
|
||||
, runFormPostNoNonce
|
||||
, runFormGet
|
||||
-- * Rendering
|
||||
, FormRender
|
||||
, renderTable
|
||||
, renderDivs
|
||||
) where
|
||||
|
||||
import Yesod.Form.Types
|
||||
import Yesod.Form.Fields (FormMessage (MsgCsrfWarning, MsgValueRequired))
|
||||
import Data.Text (Text, pack)
|
||||
import Control.Monad.Trans.RWS (ask, get, put, runRWST, tell, evalRWST)
|
||||
import Control.Monad.Trans.Class (lift)
|
||||
import Control.Monad (liftM, join)
|
||||
import Text.Blaze (Html, toHtml)
|
||||
import Yesod.Handler (GHandler, GGHandler, getRequest, runRequestBody, newIdent, getYesod)
|
||||
import Yesod.Core (RenderMessage)
|
||||
import Yesod.Widget (GGWidget, whamlet)
|
||||
import Yesod.Request (reqNonce, reqWaiRequest, reqGetParams, languages)
|
||||
import Network.Wai (requestMethod)
|
||||
import Text.Hamlet (html)
|
||||
import Data.Monoid (mempty)
|
||||
import Data.Maybe (listToMaybe)
|
||||
import Yesod.Message (RenderMessage (..))
|
||||
|
||||
#if __GLASGOW_HASKELL__ >= 700
|
||||
#define WHAMLET whamlet
|
||||
#define HTML html
|
||||
#else
|
||||
#define HTML $html
|
||||
#define WHAMLET $whamlet
|
||||
#endif
|
||||
|
||||
-- | Get a unique identifier.
|
||||
newFormIdent :: Monad m => Form msg m Text
|
||||
newFormIdent = do
|
||||
i <- get
|
||||
let i' = incrInts i
|
||||
put i'
|
||||
return $ pack $ 'f' : show i'
|
||||
where
|
||||
incrInts (IntSingle i) = IntSingle $ i + 1
|
||||
incrInts (IntCons i is) = (i + 1) `IntCons` is
|
||||
|
||||
formToAForm :: Monad m => Form msg m (FormResult a, xml) -> AForm ([xml] -> [xml]) msg m a
|
||||
formToAForm form = AForm $ \(master, langs) env ints -> do
|
||||
((a, xml), ints', enc) <- runRWST form (env, master, langs) ints
|
||||
return (a, (:) xml, ints', enc)
|
||||
|
||||
aFormToForm :: Monad m => AForm xml msg m a -> Form msg m (FormResult a, xml)
|
||||
aFormToForm (AForm aform) = do
|
||||
ints <- get
|
||||
(env, master, langs) <- ask
|
||||
(a, xml, ints', enc) <- lift $ aform (master, langs) env ints
|
||||
put ints'
|
||||
tell enc
|
||||
return (a, xml)
|
||||
|
||||
askParams :: Monad m => Form msg m (Maybe Env)
|
||||
askParams = do
|
||||
(x, _, _) <- ask
|
||||
return $ liftM fst x
|
||||
|
||||
askFiles :: Monad m => Form msg m (Maybe FileEnv)
|
||||
askFiles = do
|
||||
(x, _, _) <- ask
|
||||
return $ liftM snd x
|
||||
|
||||
mreq :: (Monad m, RenderMessage master msg, RenderMessage master msg2, RenderMessage master FormMessage)
|
||||
=> Field xml msg a -> FieldSettings msg2 -> Maybe a
|
||||
-> Form master (GGHandler sub master m) (FormResult a, FieldView xml)
|
||||
mreq field fs mdef = mhelper field fs mdef (\m l -> FormFailure [renderMessage m l MsgValueRequired]) FormSuccess True
|
||||
|
||||
mopt :: (Monad m, RenderMessage master msg, RenderMessage master msg2)
|
||||
=> Field xml msg a -> FieldSettings msg2 -> Maybe (Maybe a)
|
||||
-> Form master (GGHandler sub master m) (FormResult (Maybe a), FieldView xml)
|
||||
mopt field fs mdef = mhelper field fs (join mdef) (const $ const $ FormSuccess Nothing) (FormSuccess . Just) False
|
||||
|
||||
mhelper :: (Monad m, RenderMessage master msg, RenderMessage master msg2)
|
||||
=> Field xml msg a
|
||||
-> FieldSettings msg2
|
||||
-> Maybe a
|
||||
-> (master -> [Text] -> FormResult b) -- ^ on missing
|
||||
-> (a -> FormResult b) -- ^ on success
|
||||
-> Bool -- ^ is it required?
|
||||
-> Form master (GGHandler sub master m) (FormResult b, FieldView xml)
|
||||
|
||||
mhelper Field {..} FieldSettings {..} mdef onMissing onFound isReq = do
|
||||
mp <- askParams
|
||||
name <- maybe newFormIdent return fsName
|
||||
theId <- lift $ maybe (liftM pack newIdent) return fsId
|
||||
(_, master, langs) <- ask
|
||||
let mr2 = renderMessage master langs
|
||||
let (res, val) =
|
||||
case mp of
|
||||
Nothing -> (FormMissing, maybe (Left "") Right mdef)
|
||||
Just p ->
|
||||
let mvals = map snd $ filter (\(n,_) -> n == name) p
|
||||
in case fieldParse mvals of
|
||||
Left e -> (FormFailure [renderMessage master langs e], maybe (Left "") Left (listToMaybe mvals))
|
||||
Right mx ->
|
||||
case mx of
|
||||
Nothing -> (onMissing master langs, Left "")
|
||||
Just x -> (onFound x, Right x)
|
||||
return (res, FieldView
|
||||
{ fvLabel = toHtml $ mr2 fsLabel
|
||||
, fvTooltip = fmap toHtml $ fmap mr2 fsTooltip
|
||||
, fvId = theId
|
||||
, fvInput = fieldView theId name val isReq
|
||||
, fvErrors =
|
||||
case res of
|
||||
FormFailure [e] -> Just $ toHtml e
|
||||
_ -> Nothing
|
||||
, fvRequired = isReq
|
||||
})
|
||||
|
||||
areq :: (Monad m, RenderMessage master msg1, RenderMessage master msg2, RenderMessage master FormMessage)
|
||||
=> Field xml msg1 a -> FieldSettings msg2 -> Maybe a
|
||||
-> AForm ([FieldView xml] -> [FieldView xml]) master (GGHandler sub master m) a
|
||||
areq a b = formToAForm . mreq a b
|
||||
|
||||
aopt :: (Monad m, RenderMessage master msg1, RenderMessage master msg2)
|
||||
=> Field xml msg1 a -> FieldSettings msg2 -> Maybe (Maybe a)
|
||||
-> AForm ([FieldView xml] -> [FieldView xml]) master (GGHandler sub master m) (Maybe a)
|
||||
aopt a b = formToAForm . mopt a b
|
||||
|
||||
runFormGeneric :: Monad m => Form master m a -> master -> [Text] -> Maybe (Env, FileEnv) -> m (a, Enctype)
|
||||
runFormGeneric form master langs env = evalRWST form (env, master, langs) (IntSingle 1)
|
||||
|
||||
runFormPost :: RenderMessage master FormMessage
|
||||
=> (Html -> Form master (GHandler sub master) (FormResult a, xml)) -> GHandler sub master ((FormResult a, xml), Enctype)
|
||||
runFormPost form = do
|
||||
req <- getRequest
|
||||
let nonceKey = "_nonce"
|
||||
let nonce =
|
||||
case reqNonce req of
|
||||
Nothing -> mempty
|
||||
Just n -> [HTML|<input type=hidden name=#{nonceKey} value=#{n}>|]
|
||||
env <- if requestMethod (reqWaiRequest req) == "GET"
|
||||
then return Nothing
|
||||
else fmap Just runRequestBody
|
||||
m <- getYesod
|
||||
langs <- languages
|
||||
((res, xml), enctype) <- runFormGeneric (form nonce) m langs env
|
||||
let res' =
|
||||
case (res, env) of
|
||||
(FormSuccess{}, Just (params, _))
|
||||
| lookup nonceKey params /= reqNonce req ->
|
||||
FormFailure [renderMessage m langs MsgCsrfWarning]
|
||||
_ -> res
|
||||
return ((res', xml), enctype)
|
||||
|
||||
runFormPostNoNonce :: (Html -> Form master (GHandler sub master) (FormResult a, xml)) -> GHandler sub master ((FormResult a, xml), Enctype)
|
||||
runFormPostNoNonce form = do
|
||||
req <- getRequest
|
||||
env <- if requestMethod (reqWaiRequest req) == "GET"
|
||||
then return Nothing
|
||||
else fmap Just runRequestBody
|
||||
langs <- languages
|
||||
m <- getYesod
|
||||
runFormGeneric (form mempty) m langs env
|
||||
|
||||
runFormGet :: Monad m => (Html -> Form master (GGHandler sub master m) a) -> GGHandler sub master m (a, Enctype)
|
||||
runFormGet form = do
|
||||
let key = "_hasdata"
|
||||
let fragment = [HTML|<input type=hidden name=#{key}>|]
|
||||
gets <- liftM reqGetParams getRequest
|
||||
let env =
|
||||
case lookup key gets of
|
||||
Nothing -> Nothing
|
||||
Just _ -> Just (gets, [])
|
||||
langs <- languages
|
||||
m <- getYesod
|
||||
runFormGeneric (form fragment) m langs env
|
||||
|
||||
type FormRender master msg m a =
|
||||
AForm ([FieldView (GGWidget master m ())] -> [FieldView (GGWidget master m ())]) msg m a
|
||||
-> Html
|
||||
-> Form msg m (FormResult a, GGWidget master m ())
|
||||
|
||||
renderTable, renderDivs :: Monad m => FormRender master msg m a
|
||||
renderTable aform fragment = do
|
||||
(res, views') <- aFormToForm aform
|
||||
let views = views' []
|
||||
-- FIXME non-valid HTML
|
||||
let widget = [WHAMLET|
|
||||
\#{fragment}
|
||||
$forall view <- views
|
||||
<tr :fvRequired view:.required :not $ fvRequired view:.optional>
|
||||
<td>
|
||||
<label for=#{fvId view}>#{fvLabel view}
|
||||
$maybe tt <- fvTooltip view
|
||||
<div .tooltip>#{tt}
|
||||
<td>^{fvInput view}
|
||||
$maybe err <- fvErrors view
|
||||
<td .errors>#{err}
|
||||
|]
|
||||
return (res, widget)
|
||||
|
||||
renderDivs aform fragment = do
|
||||
(res, views') <- aFormToForm aform
|
||||
let views = views' []
|
||||
let widget = [WHAMLET|
|
||||
\#{fragment}
|
||||
$forall view <- views
|
||||
<div :fvRequired view:.required :not $ fvRequired view:.optional>
|
||||
<label for=#{fvId view}>#{fvLabel view}
|
||||
$maybe tt <- fvTooltip view
|
||||
<div .tooltip>#{tt}
|
||||
^{fvInput view}
|
||||
$maybe err <- fvErrors view
|
||||
<div .errors>#{err}
|
||||
|]
|
||||
return (res, widget)
|
||||
65
yesod-form/Yesod/Form/Input.hs
Normal file
65
yesod-form/Yesod/Form/Input.hs
Normal file
@ -0,0 +1,65 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
module Yesod.Form.Input
|
||||
( FormInput (..)
|
||||
, runInputGet
|
||||
, runInputPost
|
||||
, ireq
|
||||
, iopt
|
||||
) where
|
||||
|
||||
import Yesod.Form.Types
|
||||
import Yesod.Form.Fields (FormMessage (MsgInputNotFound))
|
||||
import Data.Text (Text)
|
||||
import Control.Applicative (Applicative (..))
|
||||
import Yesod.Handler (GHandler, GGHandler, invalidArgs, runRequestBody, getRequest, getYesod)
|
||||
import Yesod.Request (reqGetParams, languages)
|
||||
import Control.Monad (liftM)
|
||||
import Yesod.Widget (GWidget)
|
||||
import Yesod.Message (RenderMessage (..))
|
||||
|
||||
type DText = [Text] -> [Text]
|
||||
newtype FormInput master a = FormInput { unFormInput :: master -> [Text] -> Env -> Either DText a }
|
||||
instance Functor (FormInput master) where
|
||||
fmap a (FormInput f) = FormInput $ \c d e -> either Left (Right . a) $ f c d e
|
||||
instance Applicative (FormInput master) where
|
||||
pure = FormInput . const . const . const . Right
|
||||
(FormInput f) <*> (FormInput x) = FormInput $ \c d e ->
|
||||
case (f c d e, x c d e) of
|
||||
(Left a, Left b) -> Left $ a . b
|
||||
(Left a, _) -> Left a
|
||||
(_, Left b) -> Left b
|
||||
(Right a, Right b) -> Right $ a b
|
||||
|
||||
ireq :: (RenderMessage master msg, RenderMessage master FormMessage) => Field (GWidget sub master ()) msg a -> Text -> FormInput master a
|
||||
ireq field name = FormInput $ \m l env ->
|
||||
let filteredEnv = map snd $ filter (\y -> fst y == name) env
|
||||
in case fieldParse field $ filteredEnv of
|
||||
Left e -> Left $ (:) $ renderMessage m l e
|
||||
Right Nothing -> Left $ (:) $ renderMessage m l $ MsgInputNotFound name
|
||||
Right (Just a) -> Right a
|
||||
|
||||
iopt :: RenderMessage master msg => Field (GWidget sub master ()) msg a -> Text -> FormInput master (Maybe a)
|
||||
iopt field name = FormInput $ \m l env ->
|
||||
let filteredEnv = map snd $ filter (\y -> fst y == name) env
|
||||
in case fieldParse field $ filteredEnv of
|
||||
Left e -> Left $ (:) $ renderMessage m l e
|
||||
Right x -> Right x
|
||||
|
||||
runInputGet :: Monad monad => FormInput master a -> GGHandler sub master monad a
|
||||
runInputGet (FormInput f) = do
|
||||
env <- liftM reqGetParams getRequest
|
||||
m <- getYesod
|
||||
l <- languages
|
||||
case f m l env of
|
||||
Left errs -> invalidArgs $ errs []
|
||||
Right x -> return x
|
||||
|
||||
runInputPost :: FormInput master a -> GHandler sub master a
|
||||
runInputPost (FormInput f) = do
|
||||
env <- liftM fst runRequestBody
|
||||
m <- getYesod
|
||||
l <- languages
|
||||
case f m l env of
|
||||
Left errs -> invalidArgs $ errs []
|
||||
Right x -> return x
|
||||
205
yesod-form/Yesod/Form/Jquery.hs
Normal file
205
yesod-form/Yesod/Form/Jquery.hs
Normal file
@ -0,0 +1,205 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE NoMonomorphismRestriction #-} -- FIXME remove
|
||||
-- | Some fields spiced up with jQuery UI.
|
||||
module Yesod.Form.Jquery
|
||||
( YesodJquery (..)
|
||||
, jqueryDayField
|
||||
, jqueryDayTimeField
|
||||
, jqueryAutocompleteField
|
||||
, googleHostedJqueryUiCss
|
||||
, JqueryDaySettings (..)
|
||||
, Default (..)
|
||||
) where
|
||||
|
||||
import Yesod.Handler
|
||||
import Yesod.Form
|
||||
import Yesod.Widget
|
||||
import Data.Time (UTCTime (..), Day, TimeOfDay (..), timeOfDayToTime,
|
||||
timeToTimeOfDay)
|
||||
import Data.Char (isSpace)
|
||||
import Data.Default
|
||||
import Text.Hamlet (html)
|
||||
import Text.Julius (julius)
|
||||
import Control.Monad.Trans.Class (lift)
|
||||
import Data.Text (Text, pack, unpack)
|
||||
import Data.Monoid (mconcat)
|
||||
|
||||
#if __GLASGOW_HASKELL__ >= 700
|
||||
#define HTML html
|
||||
#define HAMLET hamlet
|
||||
#define CASSIUS cassius
|
||||
#define JULIUS julius
|
||||
#else
|
||||
#define HTML $html
|
||||
#define HAMLET $hamlet
|
||||
#define CASSIUS $cassius
|
||||
#define JULIUS $julius
|
||||
#endif
|
||||
|
||||
-- | Gets the Google hosted jQuery UI 1.8 CSS file with the given theme.
|
||||
googleHostedJqueryUiCss :: Text -> Text
|
||||
googleHostedJqueryUiCss theme = mconcat
|
||||
[ "http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/themes/"
|
||||
, theme
|
||||
, "/jquery-ui.css"
|
||||
]
|
||||
|
||||
class YesodJquery a where
|
||||
-- | The jQuery 1.4 Javascript file.
|
||||
urlJqueryJs :: a -> Either (Route a) Text
|
||||
urlJqueryJs _ = Right "http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js"
|
||||
|
||||
-- | The jQuery UI 1.8 Javascript file.
|
||||
urlJqueryUiJs :: a -> Either (Route a) Text
|
||||
urlJqueryUiJs _ = Right "http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/jquery-ui.min.js"
|
||||
|
||||
-- | The jQuery UI 1.8 CSS file; defaults to cupertino theme.
|
||||
urlJqueryUiCss :: a -> Either (Route a) Text
|
||||
urlJqueryUiCss _ = Right $ googleHostedJqueryUiCss "cupertino"
|
||||
|
||||
-- | jQuery UI time picker add-on.
|
||||
urlJqueryUiDateTimePicker :: a -> Either (Route a) Text
|
||||
urlJqueryUiDateTimePicker _ = Right "http://github.com/gregwebs/jquery.ui.datetimepicker/raw/master/jquery.ui.datetimepicker.js"
|
||||
|
||||
blank :: (Text -> Either msg a) -> [Text] -> Either msg (Maybe a)
|
||||
blank _ [] = Right Nothing
|
||||
blank _ ("":_) = Right Nothing
|
||||
blank f (x:_) = either Left (Right . Just) $ f x
|
||||
|
||||
jqueryDayField :: (YesodJquery master) => JqueryDaySettings -> Field (GWidget sub master ()) FormMessage Day
|
||||
jqueryDayField jds = Field
|
||||
{ fieldParse = blank $ maybe
|
||||
(Left MsgInvalidDay)
|
||||
Right
|
||||
. readMay
|
||||
. unpack
|
||||
, fieldView = \theId name val isReq -> do
|
||||
addHtml [HTML|\
|
||||
<input id="#{theId}" name="#{name}" type="date" :isReq:required="" value="#{showVal val}">
|
||||
|]
|
||||
addScript' urlJqueryJs
|
||||
addScript' urlJqueryUiJs
|
||||
addStylesheet' urlJqueryUiCss
|
||||
addJulius [JULIUS|
|
||||
$(function(){$("##{theId}").datepicker({
|
||||
dateFormat:'yy-mm-dd',
|
||||
changeMonth:#{jsBool $ jdsChangeMonth jds},
|
||||
changeYear:#{jsBool $ jdsChangeYear jds},
|
||||
numberOfMonths:#{mos $ jdsNumberOfMonths jds},
|
||||
yearRange:"#{jdsYearRange jds}"
|
||||
})});
|
||||
|]
|
||||
}
|
||||
where
|
||||
showVal = either id (pack . show)
|
||||
jsBool True = "true" :: Text
|
||||
jsBool False = "false" :: Text
|
||||
mos (Left i) = show i
|
||||
mos (Right (x, y)) = concat
|
||||
[ "["
|
||||
, show x
|
||||
, ","
|
||||
, show y
|
||||
, "]"
|
||||
]
|
||||
|
||||
ifRight :: Either a b -> (b -> c) -> Either a c
|
||||
ifRight e f = case e of
|
||||
Left l -> Left l
|
||||
Right r -> Right $ f r
|
||||
|
||||
showLeadingZero :: (Show a) => a -> String
|
||||
showLeadingZero time = let t = show time in if length t == 1 then "0" ++ t else t
|
||||
|
||||
-- use A.M/P.M and drop seconds and "UTC" (as opposed to normal UTCTime show)
|
||||
jqueryDayTimeUTCTime :: UTCTime -> String
|
||||
jqueryDayTimeUTCTime (UTCTime day utcTime) =
|
||||
let timeOfDay = timeToTimeOfDay utcTime
|
||||
in (replace '-' '/' (show day)) ++ " " ++ showTimeOfDay timeOfDay
|
||||
where
|
||||
showTimeOfDay (TimeOfDay hour minute _) =
|
||||
let (h, apm) = if hour < 12 then (hour, "AM") else (hour - 12, "PM")
|
||||
in (showLeadingZero h) ++ ":" ++ (showLeadingZero minute) ++ " " ++ apm
|
||||
|
||||
jqueryDayTimeField :: YesodJquery master => Field (GWidget sub master ()) FormMessage UTCTime
|
||||
jqueryDayTimeField = Field
|
||||
{ fieldParse = blank $ parseUTCTime . unpack
|
||||
, fieldView = \theId name val isReq -> do
|
||||
addHtml [HTML|\
|
||||
<input id="#{theId}" name="#{name}" :isReq:required="" value="#{showVal val}">
|
||||
|]
|
||||
addScript' urlJqueryJs
|
||||
addScript' urlJqueryUiJs
|
||||
addScript' urlJqueryUiDateTimePicker
|
||||
addStylesheet' urlJqueryUiCss
|
||||
addJulius [JULIUS|
|
||||
$(function(){$("##{theId}").datetimepicker({dateFormat : "yyyy/mm/dd hh:MM TT"})});
|
||||
|]
|
||||
}
|
||||
where
|
||||
showVal = either id (pack . jqueryDayTimeUTCTime)
|
||||
|
||||
parseUTCTime :: String -> Either FormMessage UTCTime
|
||||
parseUTCTime s =
|
||||
let (dateS, timeS') = break isSpace (dropWhile isSpace s)
|
||||
timeS = drop 1 timeS'
|
||||
dateE = parseDate dateS
|
||||
in case dateE of
|
||||
Left l -> Left l
|
||||
Right date ->
|
||||
ifRight (parseTime timeS)
|
||||
(UTCTime date . timeOfDayToTime)
|
||||
|
||||
jqueryAutocompleteField :: YesodJquery master => Route master -> Field (GWidget sub master ()) FormMessage Text
|
||||
jqueryAutocompleteField src = Field
|
||||
{ fieldParse = blank $ Right
|
||||
, fieldView = \theId name val isReq -> do
|
||||
addHtml [HTML|\
|
||||
<input id="#{theId}" name="#{name}" type="text" :isReq:required="" value="#{either id id val}" .autocomplete>
|
||||
|]
|
||||
addScript' urlJqueryJs
|
||||
addScript' urlJqueryUiJs
|
||||
addStylesheet' urlJqueryUiCss
|
||||
addJulius [JULIUS|
|
||||
$(function(){$("##{theId}").autocomplete({source:"@{src}",minLength:2})});
|
||||
|]
|
||||
}
|
||||
|
||||
addScript' :: Monad m => (t -> Either (Route master) Text) -> GGWidget master (GGHandler sub t m) ()
|
||||
addScript' f = do
|
||||
y <- lift getYesod
|
||||
addScriptEither $ f y
|
||||
|
||||
addStylesheet' :: (y -> Either (Route y) Text) -> GWidget sub y ()
|
||||
addStylesheet' f = do
|
||||
y <- lift getYesod
|
||||
addStylesheetEither $ f y
|
||||
|
||||
readMay :: Read a => String -> Maybe a
|
||||
readMay s = case reads s of
|
||||
(x, _):_ -> Just x
|
||||
[] -> Nothing
|
||||
|
||||
-- | Replaces all instances of a value in a list by another value.
|
||||
-- from http://hackage.haskell.org/packages/archive/cgi/3001.1.7.1/doc/html/src/Network-CGI-Protocol.html#replace
|
||||
replace :: Eq a => a -> a -> [a] -> [a]
|
||||
replace x y = map (\z -> if z == x then y else z)
|
||||
|
||||
data JqueryDaySettings = JqueryDaySettings
|
||||
{ jdsChangeMonth :: Bool
|
||||
, jdsChangeYear :: Bool
|
||||
, jdsYearRange :: String
|
||||
, jdsNumberOfMonths :: Either Int (Int, Int)
|
||||
}
|
||||
|
||||
instance Default JqueryDaySettings where
|
||||
def = JqueryDaySettings
|
||||
{ jdsChangeMonth = False
|
||||
, jdsChangeYear = False
|
||||
, jdsYearRange = "c-10:c+10"
|
||||
, jdsNumberOfMonths = Left 1
|
||||
}
|
||||
155
yesod-form/Yesod/Form/MassInput.hs
Normal file
155
yesod-form/Yesod/Form/MassInput.hs
Normal file
@ -0,0 +1,155 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
module Yesod.Form.MassInput
|
||||
( inputList
|
||||
, massDivs
|
||||
, massTable
|
||||
) where
|
||||
|
||||
import Yesod.Form.Types
|
||||
import Yesod.Form.Functions
|
||||
import Yesod.Form.Fields (boolField, FormMessage)
|
||||
import Yesod.Widget (GGWidget, whamlet)
|
||||
import Yesod.Message (RenderMessage)
|
||||
import Yesod.Handler (newIdent, GGHandler)
|
||||
import Text.Blaze (Html)
|
||||
import Control.Monad.Trans.Class (lift)
|
||||
import Data.Text (pack, Text)
|
||||
import Control.Monad.Trans.RWS (get, put, ask)
|
||||
import Data.Maybe (fromMaybe, catMaybes)
|
||||
import Data.Text.Read (decimal)
|
||||
import Control.Monad (liftM)
|
||||
import Data.Either (partitionEithers)
|
||||
import Data.Traversable (sequenceA)
|
||||
|
||||
#if __GLASGOW_HASKELL__ >= 700
|
||||
#define WHAMLET whamlet
|
||||
#define HTML html
|
||||
#else
|
||||
#define HTML $html
|
||||
#define WHAMLET $whamlet
|
||||
#endif
|
||||
|
||||
down 0 = return ()
|
||||
down i | i < 0 = error "called down with a negative number"
|
||||
down i = do
|
||||
is <- get
|
||||
put $ IntCons 0 is
|
||||
down $ i - 1
|
||||
|
||||
up 0 = return ()
|
||||
up i | i < 0 = error "called down with a negative number"
|
||||
up i = do
|
||||
is <- get
|
||||
case is of
|
||||
IntSingle _ -> error "up on IntSingle"
|
||||
IntCons _ is' -> put is' >> newFormIdent >> return ()
|
||||
up $ i - 1
|
||||
|
||||
inputList :: (Monad mo, m ~ GGHandler sub master mo, xml ~ GGWidget master (GGHandler sub master mo) (), RenderMessage master FormMessage)
|
||||
=> Html
|
||||
-> ([[FieldView xml]] -> xml)
|
||||
-> (Maybe a -> AForm ([FieldView xml] -> [FieldView xml]) master m a)
|
||||
-> (Maybe [a] -> AForm ([FieldView xml] -> [FieldView xml]) master m [a])
|
||||
inputList label fixXml single mdef = formToAForm $ do
|
||||
theId <- lift newIdent
|
||||
down 1
|
||||
countName <- newFormIdent
|
||||
addName <- newFormIdent
|
||||
(menv, _, _) <- ask
|
||||
let readInt t =
|
||||
case decimal t of
|
||||
Right (i, "") -> Just i
|
||||
_ -> Nothing
|
||||
let vals =
|
||||
case menv of
|
||||
Nothing -> map Just $ fromMaybe [] mdef
|
||||
Just (env, _) ->
|
||||
let toAdd = maybe False (const True) $ lookup addName env
|
||||
count' = fromMaybe 0 $ lookup countName env >>= readInt
|
||||
count = (if toAdd then 1 else 0) + count'
|
||||
in replicate count Nothing
|
||||
let count = length vals
|
||||
(res, xmls, views) <- liftM fixme $ mapM (withDelete . single) vals
|
||||
up 1
|
||||
return (res, FieldView
|
||||
{ fvLabel = label
|
||||
, fvTooltip = Nothing
|
||||
, fvId = pack theId
|
||||
, fvInput = [WHAMLET|
|
||||
^{fixXml views}
|
||||
<p>
|
||||
$forall xml <- xmls
|
||||
^{xml}
|
||||
<input .count type=hidden name=#{countName} value=#{count}>
|
||||
<input type=checkbox name=#{addName}>
|
||||
Add another row
|
||||
|]
|
||||
, fvErrors = Nothing
|
||||
, fvRequired = False
|
||||
})
|
||||
|
||||
withDelete :: (xml ~ GGWidget master m (), m ~ GGHandler sub master mo, Monad mo, RenderMessage master FormMessage)
|
||||
=> AForm ([FieldView xml] -> [FieldView xml]) master m a
|
||||
-> Form master m (Either xml (FormResult a, [FieldView xml]))
|
||||
withDelete af = do
|
||||
down 1
|
||||
deleteName <- newFormIdent
|
||||
(menv, _, _) <- ask
|
||||
res <- case menv >>= lookup deleteName . fst of
|
||||
Just "yes" -> return $ Left [WHAMLET|<input type=hidden name=#{deleteName} value=yes>|]
|
||||
_ -> do
|
||||
(_, xml2) <- aFormToForm $ areq boolField FieldSettings
|
||||
{ fsLabel = "Delete?" :: Text -- FIXME
|
||||
, fsTooltip = Nothing
|
||||
, fsName = Just deleteName
|
||||
, fsId = Nothing
|
||||
} $ Just False -- TRANS
|
||||
(res, xml) <- aFormToForm af
|
||||
return $ Right (res, xml [] ++ xml2 []) -- FIXME shouldn't need ++
|
||||
up 1
|
||||
return res
|
||||
|
||||
fixme :: (xml ~ GGWidget master (GGHandler sub master mo) ())
|
||||
=> [Either xml (FormResult a, [FieldView xml])]
|
||||
-> (FormResult [a], [xml], [[FieldView xml]])
|
||||
fixme eithers =
|
||||
(res, xmls, map snd rest)
|
||||
where
|
||||
(xmls, rest) = partitionEithers eithers
|
||||
res = sequenceA $ map fst rest
|
||||
|
||||
massDivs, massTable
|
||||
:: Monad m
|
||||
=> [[FieldView (GGWidget master m ())]]
|
||||
-> GGWidget master m ()
|
||||
massDivs viewss = [WHAMLET|
|
||||
$forall views <- viewss
|
||||
<fieldset>
|
||||
$forall view <- views
|
||||
<div :fvRequired view:.required :not $ fvRequired view:.optional>
|
||||
<label for=#{fvId view}>#{fvLabel view}
|
||||
$maybe tt <- fvTooltip view
|
||||
<div .tooltip>#{tt}
|
||||
^{fvInput view}
|
||||
$maybe err <- fvErrors view
|
||||
<div .errors>#{err}
|
||||
|]
|
||||
|
||||
massTable viewss = [WHAMLET|
|
||||
$forall views <- viewss
|
||||
<fieldset>
|
||||
<table>
|
||||
$forall view <- views
|
||||
<tr :fvRequired view:.required :not $ fvRequired view:.optional>
|
||||
<td>
|
||||
<label for=#{fvId view}>#{fvLabel view}
|
||||
$maybe tt <- fvTooltip view
|
||||
<div .tooltip>#{tt}
|
||||
<td>^{fvInput view}
|
||||
$maybe err <- fvErrors view
|
||||
<td .errors>#{err}
|
||||
|]
|
||||
64
yesod-form/Yesod/Form/Nic.hs
Normal file
64
yesod-form/Yesod/Form/Nic.hs
Normal file
@ -0,0 +1,64 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE NoMonomorphismRestriction #-} -- FIXME remove
|
||||
-- | Provide the user with a rich text editor.
|
||||
module Yesod.Form.Nic
|
||||
( YesodNic (..)
|
||||
, nicHtmlField
|
||||
) where
|
||||
|
||||
import Yesod.Handler
|
||||
import Yesod.Form
|
||||
import Yesod.Widget
|
||||
import Text.HTML.SanitizeXSS (sanitizeBalance)
|
||||
import Text.Hamlet (Html, html)
|
||||
import Text.Julius (julius)
|
||||
import Text.Blaze.Renderer.String (renderHtml)
|
||||
import Text.Blaze (preEscapedString)
|
||||
import Control.Monad.Trans.Class (lift)
|
||||
import Data.Text (Text, pack, unpack)
|
||||
|
||||
class YesodNic a where
|
||||
-- | NIC Editor Javascript file.
|
||||
urlNicEdit :: a -> Either (Route a) Text
|
||||
urlNicEdit _ = Right "http://js.nicedit.com/nicEdit-latest.js"
|
||||
|
||||
blank :: (Text -> Either msg a) -> [Text] -> Either msg (Maybe a)
|
||||
blank _ [] = Right Nothing
|
||||
blank _ ("":_) = Right Nothing
|
||||
blank f (x:_) = either Left (Right . Just) $ f x
|
||||
|
||||
|
||||
|
||||
nicHtmlField :: YesodNic master => Field (GWidget sub master ()) msg Html
|
||||
nicHtmlField = Field
|
||||
{ fieldParse = blank $ Right . preEscapedString . sanitizeBalance . unpack -- FIXME
|
||||
, fieldView = \theId name val _isReq -> do
|
||||
addHtml
|
||||
#if __GLASGOW_HASKELL__ >= 700
|
||||
[html|
|
||||
#else
|
||||
[$html|
|
||||
#endif
|
||||
<textarea id="#{theId}" name="#{name}" .html>#{showVal val}
|
||||
|]
|
||||
addScript' urlNicEdit
|
||||
addJulius
|
||||
#if __GLASGOW_HASKELL__ >= 700
|
||||
[julius|
|
||||
#else
|
||||
[$julius|
|
||||
#endif
|
||||
bkLib.onDomLoaded(function(){new nicEditor({fullPanel:true}).panelInstance("#{theId}")});
|
||||
|]
|
||||
}
|
||||
where
|
||||
showVal = either id (pack . renderHtml)
|
||||
|
||||
addScript' :: (y -> Either (Route y) Text) -> GWidget sub y ()
|
||||
addScript' f = do
|
||||
y <- lift getYesod
|
||||
addScriptEither $ f y
|
||||
123
yesod-form/Yesod/Form/Types.hs
Normal file
123
yesod-form/Yesod/Form/Types.hs
Normal file
@ -0,0 +1,123 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
module Yesod.Form.Types
|
||||
( -- * Helpers
|
||||
Enctype (..)
|
||||
, FormResult (..)
|
||||
, Env
|
||||
, FileEnv
|
||||
, Ints (..)
|
||||
-- * Form
|
||||
, Form
|
||||
, AForm (..)
|
||||
-- * Build forms
|
||||
, Field (..)
|
||||
, FieldSettings (..)
|
||||
, FieldView (..)
|
||||
) where
|
||||
|
||||
import Control.Monad.Trans.RWS (RWST)
|
||||
import Yesod.Request (FileInfo)
|
||||
import Data.Text (Text)
|
||||
import Data.Monoid (Monoid (..))
|
||||
import Text.Blaze (Html, ToHtml (toHtml))
|
||||
import Control.Applicative ((<$>), Applicative (..))
|
||||
import Control.Monad (liftM)
|
||||
import Data.String (IsString (..))
|
||||
import Control.Monad.Trans.Class (MonadTrans (..))
|
||||
|
||||
-- | 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.
|
||||
data FormResult a = FormMissing
|
||||
| FormFailure [Text]
|
||||
| FormSuccess a
|
||||
deriving Show
|
||||
instance Functor FormResult where
|
||||
fmap _ FormMissing = FormMissing
|
||||
fmap _ (FormFailure errs) = FormFailure errs
|
||||
fmap f (FormSuccess a) = FormSuccess $ f a
|
||||
instance Applicative FormResult where
|
||||
pure = FormSuccess
|
||||
(FormSuccess f) <*> (FormSuccess g) = FormSuccess $ f g
|
||||
(FormFailure x) <*> (FormFailure y) = FormFailure $ x ++ y
|
||||
(FormFailure x) <*> _ = FormFailure x
|
||||
_ <*> (FormFailure y) = FormFailure y
|
||||
_ <*> _ = FormMissing
|
||||
instance Monoid m => Monoid (FormResult m) where
|
||||
mempty = pure mempty
|
||||
mappend x y = mappend <$> x <*> y
|
||||
|
||||
-- | The encoding type required by a form. The 'ToHtml' instance produces values
|
||||
-- that can be inserted directly into HTML.
|
||||
data Enctype = UrlEncoded | Multipart
|
||||
deriving (Eq, Enum, Bounded)
|
||||
instance ToHtml Enctype where
|
||||
toHtml UrlEncoded = "application/x-www-form-urlencoded"
|
||||
toHtml Multipart = "multipart/form-data"
|
||||
instance Monoid Enctype where
|
||||
mempty = UrlEncoded
|
||||
mappend UrlEncoded UrlEncoded = UrlEncoded
|
||||
mappend _ _ = Multipart
|
||||
|
||||
data Ints = IntCons Int Ints | IntSingle Int
|
||||
instance Show Ints where
|
||||
show (IntSingle i) = show i
|
||||
show (IntCons i is) = show i ++ ('-' : show is)
|
||||
|
||||
type Env = [(Text, Text)] -- FIXME use a Map
|
||||
type FileEnv = [(Text, FileInfo)]
|
||||
|
||||
type Form master m a = RWST (Maybe (Env, FileEnv), master, [Text]) Enctype Ints m a
|
||||
|
||||
newtype AForm xml master m a = AForm
|
||||
{ unAForm :: (master, [Text]) -> Maybe (Env, FileEnv) -> Ints -> m (FormResult a, xml, Ints, Enctype)
|
||||
}
|
||||
instance Monad m => Functor (AForm xml msg m) where
|
||||
fmap f (AForm a) =
|
||||
AForm $ \x y z -> liftM go $ a x y z
|
||||
where
|
||||
go (w, x, y, z) = (fmap f w, x, y, z)
|
||||
instance (Monad m, Monoid xml) => Applicative (AForm xml msg m) where
|
||||
pure x = AForm $ const $ const $ \ints -> return (FormSuccess x, mempty, ints, mempty)
|
||||
(AForm f) <*> (AForm g) = AForm $ \mr env ints -> do
|
||||
(a, b, ints', c) <- f mr env ints
|
||||
(x, y, ints'', z) <- g mr env ints'
|
||||
return (a <*> x, b `mappend` y, ints'', c `mappend` z)
|
||||
instance (Monad m, Monoid xml, Monoid a) => Monoid (AForm xml msg m a) where
|
||||
mempty = pure mempty
|
||||
mappend a b = mappend <$> a <*> b
|
||||
instance Monoid xml => MonadTrans (AForm xml msg) where
|
||||
lift mx = AForm $ const $ const $ \ints -> do
|
||||
x <- mx
|
||||
return (pure x, mempty, ints, mempty)
|
||||
|
||||
data FieldSettings msg = FieldSettings
|
||||
{ fsLabel :: msg
|
||||
, fsTooltip :: Maybe msg
|
||||
, fsId :: Maybe Text
|
||||
, fsName :: Maybe Text
|
||||
}
|
||||
|
||||
instance (a ~ Text) => IsString (FieldSettings a) where
|
||||
fromString s = FieldSettings (fromString s) Nothing Nothing Nothing
|
||||
|
||||
data FieldView xml = FieldView
|
||||
{ fvLabel :: Html
|
||||
, fvTooltip :: Maybe Html
|
||||
, fvId :: Text
|
||||
, fvInput :: xml
|
||||
, fvErrors :: Maybe Html
|
||||
, fvRequired :: Bool
|
||||
}
|
||||
|
||||
data Field xml msg a = Field
|
||||
{ fieldParse :: [Text] -> Either msg (Maybe a)
|
||||
, fieldView :: Text -- ^ ID
|
||||
-> Text -- ^ name
|
||||
-> Either Text a -- ^ value could be invalid text or a legitimate a
|
||||
-> Bool -- ^ required?
|
||||
-> xml
|
||||
}
|
||||
203
yesod-form/Yesod/Helpers/Crud.hs
Normal file
203
yesod-form/Yesod/Helpers/Crud.hs
Normal file
@ -0,0 +1,203 @@
|
||||
{-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE Rank2Types #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE CPP #-}
|
||||
module Yesod.Helpers.Crud
|
||||
( Item (..)
|
||||
, Crud (..)
|
||||
, CrudRoute (..)
|
||||
, defaultCrud
|
||||
) where
|
||||
|
||||
import Yesod.Core
|
||||
import Text.Hamlet
|
||||
import Yesod.Form
|
||||
import Language.Haskell.TH.Syntax
|
||||
import Yesod.Persist
|
||||
import Data.Text (Text)
|
||||
import Web.Routes.Quasi (toSinglePiece, fromSinglePiece)
|
||||
|
||||
-- | An entity which can be displayed by the Crud subsite.
|
||||
class Item a where
|
||||
-- | The title of an entity, to be displayed in the list of all entities.
|
||||
itemTitle :: a -> Text
|
||||
|
||||
-- | Defines all of the CRUD operations (Create, Read, Update, Delete)
|
||||
-- necessary to implement this subsite. When using the "Yesod.Form" module and
|
||||
-- 'ToForm' typeclass, you can probably just use 'defaultCrud'.
|
||||
data Crud master item = Crud
|
||||
{ crudSelect :: GHandler (Crud master item) master [(Key item, item)]
|
||||
, crudReplace :: Key item -> item -> GHandler (Crud master item) master ()
|
||||
, crudInsert :: item -> GHandler (Crud master item) master (Key item)
|
||||
, crudGet :: Key item -> GHandler (Crud master item) master (Maybe item)
|
||||
, crudDelete :: Key item -> GHandler (Crud master item) master ()
|
||||
}
|
||||
|
||||
mkYesodSub "Crud master item"
|
||||
[ ClassP ''Item [VarT $ mkName "item"]
|
||||
, ClassP ''SinglePiece [ConT ''Key `AppT` VarT (mkName "item")]
|
||||
, ClassP ''ToForm [VarT $ mkName "item", VarT $ mkName "master"]
|
||||
]
|
||||
#if __GLASGOW_HASKELL__ >= 700
|
||||
[parseRoutes|
|
||||
#else
|
||||
[$parseRoutes|
|
||||
#endif
|
||||
/ CrudListR GET
|
||||
/add CrudAddR GET POST
|
||||
/edit/#Text CrudEditR GET POST
|
||||
/delete/#Text CrudDeleteR GET POST
|
||||
|]
|
||||
|
||||
getCrudListR :: (Yesod master, Item item, SinglePiece (Key item))
|
||||
=> GHandler (Crud master item) master RepHtml
|
||||
getCrudListR = do
|
||||
items <- getYesodSub >>= crudSelect
|
||||
toMaster <- getRouteToMaster
|
||||
defaultLayout $ do
|
||||
setTitle "Items"
|
||||
addWidget
|
||||
#if __GLASGOW_HASKELL__ >= 700
|
||||
[hamlet|
|
||||
#else
|
||||
[$hamlet|
|
||||
#endif
|
||||
<h1>Items
|
||||
<ul>
|
||||
$forall item <- items
|
||||
<li>
|
||||
<a href="@{toMaster (CrudEditR (toSinglePiece (fst item)))}">
|
||||
\#{itemTitle (snd item)}
|
||||
<p>
|
||||
<a href="@{toMaster CrudAddR}">Add new item
|
||||
|]
|
||||
|
||||
getCrudAddR :: (Yesod master, Item item, SinglePiece (Key item),
|
||||
ToForm item master)
|
||||
=> GHandler (Crud master item) master RepHtml
|
||||
getCrudAddR = crudHelper
|
||||
"Add new"
|
||||
(Nothing :: Maybe (Key item, item))
|
||||
False
|
||||
|
||||
postCrudAddR :: (Yesod master, Item item, SinglePiece (Key item),
|
||||
ToForm item master)
|
||||
=> GHandler (Crud master item) master RepHtml
|
||||
postCrudAddR = crudHelper
|
||||
"Add new"
|
||||
(Nothing :: Maybe (Key item, item))
|
||||
True
|
||||
|
||||
getCrudEditR :: (Yesod master, Item item, SinglePiece (Key item),
|
||||
ToForm item master)
|
||||
=> Text -> GHandler (Crud master item) master RepHtml
|
||||
getCrudEditR s = do
|
||||
itemId <- maybe notFound return $ fromSinglePiece s
|
||||
crud <- getYesodSub
|
||||
item <- crudGet crud itemId >>= maybe notFound return
|
||||
crudHelper
|
||||
"Edit item"
|
||||
(Just (itemId, item))
|
||||
False
|
||||
|
||||
postCrudEditR :: (Yesod master, Item item, SinglePiece (Key item),
|
||||
ToForm item master)
|
||||
=> Text -> GHandler (Crud master item) master RepHtml
|
||||
postCrudEditR s = do
|
||||
itemId <- maybe notFound return $ fromSinglePiece s
|
||||
crud <- getYesodSub
|
||||
item <- crudGet crud itemId >>= maybe notFound return
|
||||
crudHelper
|
||||
"Edit item"
|
||||
(Just (itemId, item))
|
||||
True
|
||||
|
||||
getCrudDeleteR :: (Yesod master, Item item, SinglePiece (Key item))
|
||||
=> Text -> GHandler (Crud master item) master RepHtml
|
||||
getCrudDeleteR s = do
|
||||
itemId <- maybe notFound return $ fromSinglePiece s
|
||||
crud <- getYesodSub
|
||||
item <- crudGet crud itemId >>= maybe notFound return -- Just ensure it exists
|
||||
toMaster <- getRouteToMaster
|
||||
defaultLayout $ do
|
||||
setTitle "Confirm delete"
|
||||
addWidget
|
||||
#if __GLASGOW_HASKELL__ >= 700
|
||||
[hamlet|
|
||||
#else
|
||||
[$hamlet|
|
||||
#endif
|
||||
<form method="post" action="@{toMaster (CrudDeleteR s)}">
|
||||
<h1>Really delete?
|
||||
<p>Do you really want to delete #{itemTitle item}?
|
||||
<p>
|
||||
<input type="submit" value="Yes">
|
||||
\
|
||||
<a href="@{toMaster CrudListR}">No
|
||||
|]
|
||||
|
||||
postCrudDeleteR :: (Yesod master, Item item, SinglePiece (Key item))
|
||||
=> Text -> GHandler (Crud master item) master RepHtml
|
||||
postCrudDeleteR s = do
|
||||
itemId <- maybe notFound return $ fromSinglePiece s
|
||||
crud <- getYesodSub
|
||||
toMaster <- getRouteToMaster
|
||||
crudDelete crud itemId
|
||||
redirect RedirectTemporary $ toMaster CrudListR
|
||||
|
||||
crudHelper
|
||||
:: (Item a, Yesod master, SinglePiece (Key a), ToForm a master)
|
||||
=> Text -> Maybe (Key a, a) -> Bool
|
||||
-> GHandler (Crud master a) master RepHtml
|
||||
crudHelper title me isPost = do
|
||||
crud <- getYesodSub
|
||||
(errs, form, enctype, hidden) <- runFormPost $ toForm $ fmap snd me
|
||||
toMaster <- getRouteToMaster
|
||||
case (isPost, errs) of
|
||||
(True, FormSuccess a) -> do
|
||||
eid <- case me of
|
||||
Just (eid, _) -> do
|
||||
crudReplace crud eid a
|
||||
return eid
|
||||
Nothing -> crudInsert crud a
|
||||
redirect RedirectTemporary $ toMaster $ CrudEditR
|
||||
$ toSinglePiece eid
|
||||
_ -> return ()
|
||||
defaultLayout $ do
|
||||
setTitle $ toHtml title
|
||||
addWidget
|
||||
#if __GLASGOW_HASKELL__ >= 700
|
||||
[hamlet|
|
||||
#else
|
||||
[$hamlet|
|
||||
#endif
|
||||
<p>
|
||||
<a href="@{toMaster CrudListR}">Return to list
|
||||
<h1>#{title}
|
||||
<form method="post" enctype="#{enctype}">
|
||||
<table>
|
||||
\^{form}
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
\#{hidden}
|
||||
<input type="submit">
|
||||
$maybe e <- me
|
||||
\
|
||||
<a href="@{toMaster (CrudDeleteR (toSinglePiece (fst e)))}">Delete
|
||||
|]
|
||||
|
||||
-- | A default 'Crud' value which relies about persistent and "Yesod.Form".
|
||||
defaultCrud
|
||||
:: (PersistEntity i, PersistBackend (YesodDB a (GGHandler (Crud a i) a IO)),
|
||||
YesodPersist a)
|
||||
=> a -> Crud a i
|
||||
defaultCrud = const Crud
|
||||
{ crudSelect = runDB $ selectList [] [] 0 0
|
||||
, crudReplace = \a -> runDB . replace a
|
||||
, crudInsert = runDB . insert
|
||||
, crudGet = runDB . get
|
||||
, crudDelete = runDB . delete
|
||||
}
|
||||
74
yesod-form/hello-forms.hs
Normal file
74
yesod-form/hello-forms.hs
Normal file
@ -0,0 +1,74 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}
|
||||
import Yesod.Core
|
||||
import Yesod.Form
|
||||
import Yesod.Form.MassInput
|
||||
import Control.Applicative
|
||||
import Data.Text (Text, pack)
|
||||
import Network.Wai.Handler.Warp (run)
|
||||
|
||||
data Fruit = Apple | Banana | Pear
|
||||
deriving (Show, Enum, Bounded, Eq)
|
||||
|
||||
fruits :: [(Text, Fruit)]
|
||||
fruits = map (\x -> (pack $ show x, x)) [minBound..maxBound]
|
||||
|
||||
myForm = fixType $ runFormGet $ renderDivs $ pure (,,,,,,,,)
|
||||
<*> areq boolField "Bool field" Nothing
|
||||
<*> aopt boolField "Opt bool field" Nothing
|
||||
<*> areq textField "Text field" Nothing
|
||||
<*> areq (selectField fruits) "Select field" Nothing
|
||||
<*> aopt (selectField fruits) "Opt select field" Nothing
|
||||
<*> areq (multiSelectField fruits) "Multi select field" Nothing
|
||||
<*> aopt (multiSelectField fruits) "Opt multi select field" Nothing
|
||||
<*> aopt intField "Opt int field" Nothing
|
||||
<*> aopt (radioField fruits) "Opt radio" Nothing
|
||||
|
||||
data HelloForms = HelloForms
|
||||
type Handler = GHandler HelloForms HelloForms
|
||||
|
||||
fixType :: Handler a -> Handler a
|
||||
fixType = id
|
||||
|
||||
instance RenderMessage HelloForms FormMessage where
|
||||
renderMessage _ _ = defaultFormMessage
|
||||
|
||||
instance Yesod HelloForms where
|
||||
approot _ = ""
|
||||
|
||||
mkYesod "HelloForms" [parseRoutes|
|
||||
/ RootR GET
|
||||
/mass MassR GET
|
||||
|]
|
||||
|
||||
getRootR = do
|
||||
((res, form), enctype) <- myForm
|
||||
defaultLayout [whamlet|
|
||||
<p>Result: #{show res}
|
||||
<form enctype=#{enctype}>
|
||||
^{form}
|
||||
<div>
|
||||
<input type=submit>
|
||||
<p>
|
||||
<a href=@{MassR}>See the mass form
|
||||
|]
|
||||
|
||||
myMassForm = fixType $ runFormGet $ renderTable $ inputList "People" massTable
|
||||
(\x -> (,)
|
||||
<$> areq textField "Name" (fmap fst x)
|
||||
<*> areq intField "Age" (fmap snd x)) (Just [("Michael", 26)])
|
||||
|
||||
getMassR = do
|
||||
((res, form), enctype) <- myMassForm
|
||||
defaultLayout [whamlet|
|
||||
<p>Result: #{show res}
|
||||
<form enctype=#{enctype}>
|
||||
<table>
|
||||
^{form}
|
||||
<div>
|
||||
<input type=submit>
|
||||
<p>
|
||||
<a href=@{RootR}>See the regular form
|
||||
|]
|
||||
|
||||
main = toWaiApp HelloForms >>= run 3000
|
||||
46
yesod-form/yesod-form.cabal
Normal file
46
yesod-form/yesod-form.cabal
Normal file
@ -0,0 +1,46 @@
|
||||
name: yesod-form
|
||||
version: 0.3.0
|
||||
license: BSD3
|
||||
license-file: LICENSE
|
||||
author: Michael Snoyman <michael@snoyman.com>
|
||||
maintainer: Michael Snoyman <michael@snoyman.com>
|
||||
synopsis: Form handling support for Yesod Web Framework
|
||||
category: Web, Yesod
|
||||
stability: Stable
|
||||
cabal-version: >= 1.6
|
||||
build-type: Simple
|
||||
homepage: http://www.yesodweb.com/
|
||||
|
||||
library
|
||||
build-depends: base >= 4 && < 5
|
||||
, yesod-core >= 0.9 && < 0.10
|
||||
, time >= 1.1.4 && < 1.3
|
||||
, hamlet >= 0.9 && < 0.10
|
||||
, persistent >= 0.6 && < 0.7
|
||||
, yesod-persistent >= 0.2 && < 0.3
|
||||
, template-haskell
|
||||
, transformers >= 0.2.2 && < 0.3
|
||||
, data-default >= 0.2 && < 0.3
|
||||
, xss-sanitize >= 0.2.4 && < 0.3
|
||||
, blaze-builder >= 0.2.1 && < 0.4
|
||||
, network >= 2.2 && < 2.4
|
||||
, email-validate >= 0.2.6 && < 0.3
|
||||
, blaze-html >= 0.4 && < 0.5
|
||||
, bytestring >= 0.9 && < 0.10
|
||||
, text >= 0.7 && < 1.0
|
||||
, web-routes-quasi >= 0.7 && < 0.8
|
||||
, wai >= 0.4 && < 0.5
|
||||
exposed-modules: Yesod.Form
|
||||
Yesod.Form.Class
|
||||
Yesod.Form.Types
|
||||
Yesod.Form.Functions
|
||||
Yesod.Form.Input
|
||||
Yesod.Form.Fields
|
||||
Yesod.Form.Jquery
|
||||
Yesod.Form.Nic
|
||||
-- FIXME Yesod.Helpers.Crud
|
||||
ghc-options: -Wall
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
location: git://github.com/snoyberg/yesod-form.git
|
||||
Loading…
Reference in New Issue
Block a user