Bootstrap3.hs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. {-# LANGUAGE QuasiQuotes #-}
  2. {-# LANGUAGE TypeFamilies #-}
  3. {-# LANGUAGE OverloadedStrings #-}
  4. -- | Helper functions for creating forms when using <http://getbootstrap.com/ Bootstrap 3>.
  5. --
  6. module Yesod.Form.Bootstrap3
  7. ( -- * Example: Rendering a basic form
  8. -- $example
  9. -- * Example: Rendering a horizontal form
  10. -- $example2
  11. -- * Rendering forms
  12. renderBootstrap3
  13. , BootstrapFormLayout(..)
  14. , BootstrapGridOptions(..)
  15. -- * Field settings
  16. -- $fieldSettings
  17. , bfs
  18. , withPlaceholder
  19. , withAutofocus
  20. , withLargeInput
  21. , withSmallInput
  22. -- * Submit button
  23. , bootstrapSubmit
  24. , mbootstrapSubmit
  25. , BootstrapSubmit(..)
  26. ) where
  27. import Control.Arrow (second)
  28. import Control.Monad (liftM)
  29. import Data.Text (Text)
  30. import Data.String (IsString(..))
  31. import Yesod.Core
  32. import qualified Data.Text as T
  33. import Yesod.Form.Types
  34. import Yesod.Form.Functions
  35. -- | Create a new 'FieldSettings' with the @form-control@ class that is
  36. -- required by Bootstrap v3.
  37. --
  38. -- Since: yesod-form 1.3.8
  39. bfs :: RenderMessage site msg => msg -> FieldSettings site
  40. bfs msg =
  41. FieldSettings (SomeMessage msg) Nothing Nothing Nothing [("class", "form-control")]
  42. -- | Add a placeholder attribute to a field. If you need i18n
  43. -- for the placeholder, currently you\'ll need to do a hack and
  44. -- use 'getMessageRender' manually.
  45. --
  46. -- Since: yesod-form 1.3.8
  47. withPlaceholder :: Text -> FieldSettings site -> FieldSettings site
  48. withPlaceholder placeholder fs = fs { fsAttrs = newAttrs }
  49. where newAttrs = ("placeholder", placeholder) : fsAttrs fs
  50. -- | Add an autofocus attribute to a field.
  51. --
  52. -- Since: yesod-form 1.3.8
  53. withAutofocus :: FieldSettings site -> FieldSettings site
  54. withAutofocus fs = fs { fsAttrs = newAttrs }
  55. where newAttrs = ("autofocus", "autofocus") : fsAttrs fs
  56. -- | Add the @input-lg@ CSS class to a field.
  57. --
  58. -- Since: yesod-form 1.3.8
  59. withLargeInput :: FieldSettings site -> FieldSettings site
  60. withLargeInput fs = fs { fsAttrs = newAttrs }
  61. where newAttrs = addClass "input-lg" (fsAttrs fs)
  62. -- | Add the @input-sm@ CSS class to a field.
  63. --
  64. -- Since: yesod-form 1.3.8
  65. withSmallInput :: FieldSettings site -> FieldSettings site
  66. withSmallInput fs = fs { fsAttrs = newAttrs }
  67. where newAttrs = addClass "input-sm" (fsAttrs fs)
  68. addClass :: Text -> [(Text, Text)] -> [(Text, Text)]
  69. addClass klass [] = [("class", klass)]
  70. addClass klass (("class", old):rest) = ("class", T.concat [old, " ", klass]) : rest
  71. addClass klass (other :rest) = other : addClass klass rest
  72. -- | How many bootstrap grid columns should be taken (see
  73. -- 'BootstrapFormLayout').
  74. --
  75. -- Since: yesod-form 1.3.8
  76. data BootstrapGridOptions =
  77. ColXs !Int
  78. | ColSm !Int
  79. | ColMd !Int
  80. | ColLg !Int
  81. deriving (Eq, Ord, Show)
  82. toColumn :: BootstrapGridOptions -> String
  83. toColumn (ColXs 0) = ""
  84. toColumn (ColSm 0) = ""
  85. toColumn (ColMd 0) = ""
  86. toColumn (ColLg 0) = ""
  87. toColumn (ColXs columns) = "col-xs-" ++ show columns
  88. toColumn (ColSm columns) = "col-sm-" ++ show columns
  89. toColumn (ColMd columns) = "col-md-" ++ show columns
  90. toColumn (ColLg columns) = "col-lg-" ++ show columns
  91. toOffset :: BootstrapGridOptions -> String
  92. toOffset (ColXs 0) = ""
  93. toOffset (ColSm 0) = ""
  94. toOffset (ColMd 0) = ""
  95. toOffset (ColLg 0) = ""
  96. toOffset (ColXs columns) = "col-xs-offset-" ++ show columns
  97. toOffset (ColSm columns) = "col-sm-offset-" ++ show columns
  98. toOffset (ColMd columns) = "col-md-offset-" ++ show columns
  99. toOffset (ColLg columns) = "col-lg-offset-" ++ show columns
  100. addGO :: BootstrapGridOptions -> BootstrapGridOptions -> BootstrapGridOptions
  101. addGO (ColXs a) (ColXs b) = ColXs (a+b)
  102. addGO (ColSm a) (ColSm b) = ColSm (a+b)
  103. addGO (ColMd a) (ColMd b) = ColMd (a+b)
  104. addGO (ColLg a) (ColLg b) = ColLg (a+b)
  105. addGO a b | a > b = addGO b a
  106. addGO (ColXs a) other = addGO (ColSm a) other
  107. addGO (ColSm a) other = addGO (ColMd a) other
  108. addGO (ColMd a) other = addGO (ColLg a) other
  109. addGO (ColLg _) _ = error "Yesod.Form.Bootstrap.addGO: never here"
  110. -- | The layout used for the bootstrap form.
  111. --
  112. -- Since: yesod-form 1.3.8
  113. data BootstrapFormLayout =
  114. BootstrapBasicForm -- ^ A form with labels and inputs listed vertically. See <http://getbootstrap.com/css/#forms-example>
  115. | 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>
  116. | BootstrapHorizontalForm
  117. { bflLabelOffset :: !BootstrapGridOptions -- ^ The left <http://getbootstrap.com/css/#grid-offsetting offset> of the @\<label>@.
  118. , bflLabelSize :: !BootstrapGridOptions -- ^ The number of grid columns the @\<label>@ should use.
  119. , bflInputOffset :: !BootstrapGridOptions -- ^ The left <http://getbootstrap.com/css/#grid-offsetting offset> of the @\<input>@ from its @\<label>@.
  120. , bflInputSize :: !BootstrapGridOptions -- ^ The number of grid columns the @\<input>@ should use.
  121. } -- ^ 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>
  122. deriving (Show)
  123. -- | Render the given form using Bootstrap v3 conventions.
  124. --
  125. -- Since: yesod-form 1.3.8
  126. renderBootstrap3 :: Monad m => BootstrapFormLayout -> FormRender m a
  127. renderBootstrap3 formLayout aform fragment = do
  128. (res, views') <- aFormToForm aform
  129. let views = views' []
  130. has (Just _) = True
  131. has Nothing = False
  132. widget = [whamlet|
  133. $newline never
  134. #{fragment}
  135. $forall view <- views
  136. <div .form-group :fvRequired view:.required :not $ fvRequired view:.optional :has $ fvErrors view:.has-error>
  137. $case formLayout
  138. $of BootstrapBasicForm
  139. $if fvId view /= bootstrapSubmitId
  140. <label for=#{fvId view}>#{fvLabel view}
  141. ^{fvInput view}
  142. ^{helpWidget view}
  143. $of BootstrapInlineForm
  144. $if fvId view /= bootstrapSubmitId
  145. <label .sr-only for=#{fvId view}>#{fvLabel view}
  146. ^{fvInput view}
  147. ^{helpWidget view}
  148. $of BootstrapHorizontalForm labelOffset labelSize inputOffset inputSize
  149. $if fvId view /= bootstrapSubmitId
  150. <label .control-label .#{toOffset labelOffset} .#{toColumn labelSize} for=#{fvId view}>#{fvLabel view}
  151. <div .#{toOffset inputOffset} .#{toColumn inputSize}>
  152. ^{fvInput view}
  153. ^{helpWidget view}
  154. $else
  155. <div .#{toOffset (addGO inputOffset (addGO labelOffset labelSize))} .#{toColumn inputSize}>
  156. ^{fvInput view}
  157. ^{helpWidget view}
  158. |]
  159. return (res, widget)
  160. -- | (Internal) Render a help widget for tooltips and errors.
  161. helpWidget :: FieldView site -> WidgetT site IO ()
  162. helpWidget view = [whamlet|
  163. $maybe tt <- fvTooltip view
  164. <span .help-block>#{tt}
  165. $maybe err <- fvErrors view
  166. <span .help-block .error-block>#{err}
  167. |]
  168. -- | How the 'bootstrapSubmit' button should be rendered.
  169. --
  170. -- Since: yesod-form 1.3.8
  171. data BootstrapSubmit msg =
  172. BootstrapSubmit
  173. { bsValue :: msg
  174. -- ^ The text of the submit button.
  175. , bsClasses :: Text
  176. -- ^ Classes added to the @\<button>@.
  177. , bsAttrs :: [(Text, Text)]
  178. -- ^ Attributes added to the @\<button>@.
  179. } deriving (Show)
  180. instance IsString msg => IsString (BootstrapSubmit msg) where
  181. fromString msg = BootstrapSubmit (fromString msg) " btn-default " []
  182. -- | A Bootstrap v3 submit button disguised as a field for
  183. -- convenience. For example, if your form currently is:
  184. --
  185. -- > Person <$> areq textField "Name" Nothing
  186. -- > <*> areq textField "Surname" Nothing
  187. --
  188. -- Then just change it to:
  189. --
  190. -- > Person <$> areq textField "Name" Nothing
  191. -- > <*> areq textField "Surname" Nothing
  192. -- > <* bootstrapSubmit ("Register" :: BootstrapSubmit Text)
  193. --
  194. -- (Note that '<*' is not a typo.)
  195. --
  196. -- Alternatively, you may also just create the submit button
  197. -- manually as well in order to have more control over its
  198. -- layout.
  199. --
  200. -- Since: yesod-form 1.3.8
  201. bootstrapSubmit
  202. :: (RenderMessage site msg, HandlerSite m ~ site, MonadHandler m)
  203. => BootstrapSubmit msg -> AForm m ()
  204. bootstrapSubmit = formToAForm . liftM (second return) . mbootstrapSubmit
  205. -- | Same as 'bootstrapSubmit' but for monadic forms. This isn't
  206. -- as useful since you're not going to use 'renderBootstrap3'
  207. -- anyway.
  208. --
  209. -- Since: yesod-form 1.3.8
  210. mbootstrapSubmit
  211. :: (RenderMessage site msg, HandlerSite m ~ site, MonadHandler m)
  212. => BootstrapSubmit msg -> MForm m (FormResult (), FieldView site)
  213. mbootstrapSubmit (BootstrapSubmit msg classes attrs) =
  214. let res = FormSuccess ()
  215. widget = [whamlet|<button class="btn #{classes}" type=submit *{attrs}>_{msg}|]
  216. fv = FieldView { fvLabel = ""
  217. , fvTooltip = Nothing
  218. , fvId = bootstrapSubmitId
  219. , fvInput = widget
  220. , fvErrors = Nothing
  221. , fvRequired = False }
  222. in return (res, fv)
  223. -- | A royal hack. Magic id used to identify whether a field
  224. -- should have no label. A valid HTML4 id which is probably not
  225. -- going to clash with any other id should someone use
  226. -- 'bootstrapSubmit' outside 'renderBootstrap3'.
  227. bootstrapSubmitId :: Text
  228. bootstrapSubmitId = "b:ootstrap___unique__:::::::::::::::::submit-id"
  229. -- $example
  230. -- @\<input\>@ tags in Bootstrap 3 require the @form-control@ class,
  231. -- and so they need modified 'FieldSettings' to display correctly.
  232. --
  233. -- When creating your forms, use the 'bfs' function to add this class:
  234. --
  235. -- > personForm :: AForm Handler Person
  236. -- > personForm = Person
  237. -- > <$> areq textField (bfs ("Name" :: Text)) Nothing
  238. -- > <*> areq textField (bfs ("Surname" :: Text)) Nothing
  239. --
  240. -- That form can then be rendered into a widget using the 'renderBootstrap3' function. Here, the form is laid out vertically using 'BootstrapBasicForm':
  241. --
  242. -- > (formWidget, formEnctype) <- generateFormPost $ renderBootstrap3 BootstrapBasicForm personForm
  243. --
  244. -- And then used in Hamlet:
  245. --
  246. -- > <form role=form method=post action=@{ActionR} enctype=#{formEnctype}>
  247. -- > ^{formWidget}
  248. -- > <button type="submit" .btn .btn-default>Submit
  249. -- $example2
  250. -- Yesod.Form.Bootstrap3 also supports <http://getbootstrap.com/css/#forms-horizontal horizontal, grid based forms>.
  251. -- These forms require additional markup for the submit tag, which is provided by the 'bootstrapSubmit' function:
  252. --
  253. -- > personForm :: AForm Handler Person
  254. -- > personForm = Person
  255. -- > <$> areq textField MsgName Nothing
  256. -- > <*> areq textField MsgSurname Nothing
  257. -- > <* bootstrapSubmit (BootstrapSubmit MsgSubmit "btn-default" [("attribute-name","attribute-value")])
  258. -- > -- Note: bootstrapSubmit works with all BootstrapFormLayouts, but provides the additional markup required for Bootstrap's horizontal forms.
  259. --
  260. -- That form can be rendered with specific grid spacing:
  261. --
  262. -- > (formWidget, formEnctype) <- generateFormPost $ renderBootstrap3 (BootstrapHorizontalForm (ColSm 0) (ColSm 4) (ColSm 0) (ColSm 6)) personForm
  263. --
  264. -- And then used in Hamlet. Note the additional @form-horizontal@ class on the form, and that a manual submit tag isn't required:
  265. --
  266. -- > <form .form-horizontal role=form method=post action=@{ActionR} enctype=#{formEnctype}>
  267. -- > ^{formWidget}
  268. -- $fieldSettings
  269. -- This module comes with several methods to help customize your Bootstrap 3 @\<input\>@s.
  270. -- These functions can be chained together to apply several properties to an input:
  271. --
  272. -- > userForm :: AForm Handler UserForm
  273. -- > userForm = UserForm
  274. -- > <$> areq textField nameSettings Nothing
  275. -- > where nameSettings = withAutofocus $
  276. -- > withPlaceholder "First name" $
  277. -- > (bfs ("Name" :: Text))