diff --git a/yesod-form-multi/ChangeLog.md b/yesod-form-multi/ChangeLog.md
index c2dfe41b..b4992af5 100644
--- a/yesod-form-multi/ChangeLog.md
+++ b/yesod-form-multi/ChangeLog.md
@@ -1,5 +1,20 @@
# Changelog
+## 1.7.0
+
+[#1707](https://github.com/yesodweb/yesod/pull/1707)
+
+* Added delete buttons
+* Added support for custom text or icons inside add/delete buttons
+* Added new presets for Bootstrap + Font Awesome icons
+* Added support for more complex fields that have multiple parts stuch as radio fields
+* Improved support for fields that rely on hidden inputs like WYSIWYG editors
+* Fixed redundant class in existing Bootstrap presets
+* Fixed styling not applying to error messages on individual fields
+* Tooltips now show once at the top of the multi-field group when using `amulti`
+
## 1.6.0
-* Added `Yesod.Form.MultiInput` which supports multi-input forms without needing to submit the form to add an input field [#1601](https://github.com/yesodweb/yesod/pull/1601)
\ No newline at end of file
+[#1601](https://github.com/yesodweb/yesod/pull/1601)
+
+* Added `Yesod.Form.MultiInput` which supports multi-input forms without needing to submit the form to add an input field
\ No newline at end of file
diff --git a/yesod-form-multi/README.md b/yesod-form-multi/README.md
index 58282125..f29b7fe6 100644
--- a/yesod-form-multi/README.md
+++ b/yesod-form-multi/README.md
@@ -1,7 +1,5 @@
## yesod-form-multi
Support for creating forms in which the user can specify how many inputs to submit. Includes support for enforcing a minimum number of values.
-Intended as an alternative to `Yesod.Form.MassInput`.
-# Limitations
-- If the user adds too many fields then there is currently no support for a "delete button" although fields submitted empty are considered to be deleted.
\ No newline at end of file
+Intended as an alternative to `Yesod.Form.MassInput`.
\ No newline at end of file
diff --git a/yesod-form-multi/Yesod/Form/MultiInput.hs b/yesod-form-multi/Yesod/Form/MultiInput.hs
index 80079947..b9bc76d5 100644
--- a/yesod-form-multi/Yesod/Form/MultiInput.hs
+++ b/yesod-form-multi/Yesod/Form/MultiInput.hs
@@ -17,16 +17,19 @@ module Yesod.Form.MultiInput
, mmulti
, amulti
, bs3Settings
+ , bs3FASettings
, bs4Settings
+ , bs4FASettings
) where
import Control.Arrow (second)
import Control.Monad (liftM)
import Control.Monad.Trans.RWS (ask, tell)
import qualified Data.Map as Map
-import Data.Maybe (fromJust, listToMaybe, fromMaybe)
+import Data.Maybe (fromJust, listToMaybe, fromMaybe, isJust)
import Data.Text (Text)
import qualified Data.Text as T
+import Text.Julius (rawJS)
import Yesod.Core
import Yesod.Form.Fields (intField)
import Yesod.Form.Functions
@@ -41,43 +44,132 @@ instance ToJavascript Text where toJavascript = toJavascript . toJSON
#endif
#endif
--- @since 1.6.0
+-- | By default delete buttons have a @margin-left@ property of @0.75rem@.
+-- You can override this by specifying an alternative value in a class
+-- which is then passed inside 'MultiSettings'.
+--
+-- @since 1.7.0
data MultiSettings site = MultiSettings
- { msAddClass :: Text -- ^ Class to be applied to the "add another" button.
+ { msAddClass :: !Text -- ^ Class to be applied to the "add another" button.
+ , msDelClass :: !Text -- ^ Class to be applied to the "delete" button.
+ , msTooltipClass :: Text -- ^ Only used in applicative forms. Class to be applied to the tooltip.
+ , msWrapperErrClass :: !Text -- ^ Class to be applied to the wrapper if it's field has an error.
+ , msAddInner :: !(Maybe Html) -- ^ Inner Html of add button, defaults to "Add Another". Useful for adding icons inside buttons.
+ , msDelInner :: !(Maybe Html) -- ^ Inner Html of delete button, defaults to "Delete". Useful for adding icons inside buttons.
, msErrWidget :: Maybe (Html -> WidgetFor site ()) -- ^ Only used in applicative forms. Create a widget for displaying errors.
}
--- @since 1.6.0
+-- | The general structure of each individually generated field is as follows.
+-- There is an external wrapper element containing both an inner wrapper and any
+-- error messages that apply to that specific field. The inner wrapper contains
+-- both the field and it's corresponding delete button.
+--
+-- The structure is illustrated by the following:
+--
+-- >
+-- >
+-- > ^{fieldWidget}
+-- > ^{deleteButton}
+-- > ^{maybeErrorMessages}
+--
+-- Each wrapper element has the same class which is automatically generated. This class
+-- is returned in the 'MultiView' should you wish to change the styling. The inner wrapper
+-- uses the same class followed by @-inner@. By default the wrapper and inner wrapper has
+-- classes are as follows:
+--
+-- > .#{wrapperClass} {
+-- > margin-bottom: 1rem;
+-- > }
+-- >
+-- > .#{wrapperClass}-inner {
+-- > display: flex;
+-- > flex-direction: row;
+-- > }
+--
+-- @since 1.7.0
data MultiView site = MultiView
{ mvCounter :: FieldView site -- ^ Hidden counter field.
, mvFields :: [FieldView site] -- ^ Input fields.
, mvAddBtn :: FieldView site -- ^ Button to add another field.
+ , mvWrapperClass :: Text -- ^ Class applied to a div wrapping each field with it's delete button.
}
-- | 'MultiSettings' for Bootstrap 3.
--
-- @since 1.6.0
bs3Settings :: MultiSettings site
-bs3Settings = MultiSettings "btn btn-default" (Just errW)
+bs3Settings = MultiSettings
+ "btn btn-default"
+ "btn btn-danger"
+ "help-block"
+ "has-error"
+ Nothing Nothing (Just errW)
where
errW err =
[whamlet|
- #{err}
+ #{err}
|]
-- | 'MultiSettings' for Bootstrap 4.
--
-- @since 1.6.0
bs4Settings :: MultiSettings site
-bs4Settings = MultiSettings "btn btn-basic" (Just errW)
+bs4Settings = MultiSettings
+ "btn btn-secondary"
+ "btn btn-danger"
+ "form-text text-muted"
+ "has-error"
+ Nothing Nothing (Just errW)
where
errW err =
[whamlet|
#{err}
|]
+-- | 'MultiSettings' for Bootstrap 3 with Font Awesome 5 Icons.
+-- Uses @fa-plus@ for the add button and @fa-trash-alt@ for the delete button.
+--
+-- @since 1.7.0
+bs3FASettings :: MultiSettings site
+bs3FASettings = MultiSettings
+ "btn btn-default"
+ "btn btn-danger"
+ "help-block"
+ "has-error"
+ addIcon delIcon (Just errW)
+ where
+ addIcon = Just [shamlet||]
+ delIcon = Just [shamlet||]
+ errW err =
+ [whamlet|
+ #{err}
+ |]
+
+-- | 'MultiSettings' for Bootstrap 4 with Font Awesome 5 Icons.
+-- Uses @fa-plus@ for the add button and @fa-trash-alt@ for the delete button.
+--
+-- @since 1.7.0
+bs4FASettings :: MultiSettings site
+bs4FASettings = MultiSettings
+ "btn btn-secondary"
+ "btn btn-danger"
+ "form-text text-muted"
+ "has-error"
+ addIcon delIcon (Just errW)
+ where
+ addIcon = Just [shamlet||]
+ delIcon = Just [shamlet||]
+ errW err =
+ [whamlet|
+
#{err}
+ |]
+
-- | Applicative equivalent of 'mmulti'.
--
+-- Note about tooltips:
+-- Rather than displaying the tooltip alongside each field the
+-- tooltip is displayed once at the top of the multi-field set.
+--
-- @since 1.6.0
amulti :: (site ~ HandlerSite m, MonadHandler m, RenderMessage site FormMessage)
=> Field m a
@@ -92,20 +184,19 @@ amulti field fs defs minVals ms = formToAForm $
mform = do
(fr, MultiView {..}) <- mmulti field fs defs minVals ms
- let widget = do
+ let (fv : _) = mvFields
+ widget = do
[whamlet|
+ $maybe tooltip <- fvTooltip fv
+ #{tooltip}
+
^{fvInput mvCounter}
$forall fv <- mvFields
^{fvInput fv}
- $maybe err <- fvErrors fv
- $maybe errW <- msErrWidget ms
- ^{errW err}
-
^{fvInput mvAddBtn}
|]
- (fv : _) = mvFields
view = FieldView
{ fvLabel = fvLabel fv
, fvTooltip = Nothing
@@ -130,11 +221,10 @@ mmulti :: (site ~ HandlerSite m, MonadHandler m, RenderMessage site FormMessage)
-> Int
-> MultiSettings site
-> MForm m (FormResult [a], MultiView site)
-mmulti field fs@FieldSettings {..} defs minVals ms = do
- fieldClass <- newFormIdent
- let fs' = fs {fsAttrs = addClass fieldClass fsAttrs}
- minVals' = if minVals < 0 then 0 else minVals
- mhelperMulti field fs' fieldClass defs minVals' ms
+mmulti field fs defs minVals' ms = do
+ wrapperClass <- lift newIdent
+ let minVals = if minVals' < 0 then 0 else minVals'
+ mhelperMulti field fs wrapperClass defs minVals ms
-- Helper function, does most of the work for mmulti.
mhelperMulti :: (site ~ HandlerSite m, MonadHandler m, RenderMessage site FormMessage)
@@ -145,21 +235,22 @@ mhelperMulti :: (site ~ HandlerSite m, MonadHandler m, RenderMessage site FormMe
-> Int
-> MultiSettings site
-> MForm m (FormResult [a], MultiView site)
-mhelperMulti field@Field {..} fs@FieldSettings {..} fieldClass defs minVals MultiSettings {..} = do
+mhelperMulti field@Field {..} fs@FieldSettings {..} wrapperClass defs minVals MultiSettings {..} = do
mp <- askParams
(_, site, langs) <- ask
name <- maybe newFormIdent return fsName
- theId <- maybe newFormIdent return fsId
+ theId <- lift $ maybe newIdent return fsId
cName <- newFormIdent
- cid <- newFormIdent
- addBtnId <- newFormIdent
+ cid <- lift newIdent
+ addBtnId <- lift newIdent
+ delBtnPrefix <- lift newIdent
let mr2 = renderMessage site langs
cDef = length defs
cfs = FieldSettings "" Nothing (Just cid) (Just cName) [("hidden", "true")]
mkName i = name `T.append` (T.pack $ '-' : show i)
mkId i = theId `T.append` (T.pack $ '-' : show i)
- mkNames c = [(mkName i, mkId i) | i <- [0 .. c]]
+ mkNames c = [(i, (mkName i, mkId i)) | i <- [0 .. c]]
onMissingSucc _ _ = FormSuccess Nothing
onMissingFail m l = FormFailure [renderMessage m l MsgValueRequired]
isSuccNothing r = case r of
@@ -174,7 +265,7 @@ mhelperMulti field@Field {..} fs@FieldSettings {..} fieldClass defs minVals Mult
Just p -> mkRes intField cfs p mfs cName onMissingFail FormSuccess
-- generate counter view
- cView <- mkView intField cfs cr cid cName True
+ cView <- mkView intField cfs cr Nothing Nothing msWrapperErrClass cid cName True
let counter = case cRes of
FormSuccess c -> c
@@ -186,17 +277,71 @@ mhelperMulti field@Field {..} fs@FieldSettings {..} fieldClass defs minVals Mult
if cDef == 0
then [(FormMissing, Left "")]
else [(FormMissing, Right d) | d <- defs]
- Just p -> mapM (\n -> mkRes field fs p mfs n onMissingSucc (FormSuccess . Just)) (map fst $ mkNames counter)
+ Just p -> mapM
+ (\n -> mkRes field fs p mfs n onMissingSucc (FormSuccess . Just))
+ (map (fst . snd) $ mkNames counter)
+
+ -- delete button
+
+ -- The delFunction is included down with the add button rather than with
+ -- each delete button to ensure that the function only gets included once.
+ let delFunction = toWidget
+ [julius|
+ function deleteField_#{rawJS theId}(wrapper) {
+ var numFields = $('.#{rawJS wrapperClass}').length;
+
+ if (numFields == 1)
+ {
+ wrapper.find("*").each(function() {
+ removeVals($(this));
+ });
+ }
+ else
+ wrapper.remove();
+ }
+
+ function removeVals(e) {
+ // input types where we don't want to reset the value
+ const keepValueTypes = ["radio", "checkbox", "button"];
+
+ // uncheck any checkboxes or radio fields and empty any text boxes
+ if(e.prop('checked') == true)
+ e.prop('checked', false);
+
+ if(!keepValueTypes.includes(e.prop('type')))
+ e.val("").trigger("change");
+ // trigger change is to ensure WYSIWYG editors are updated
+ // when their hidden code field is cleared
+ }
+ |]
+
+ mkDelBtn fieldId = do
+ let delBtnId = delBtnPrefix `T.append` fieldId
+ [whamlet|
+