Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add validation functions for textArea and dropdown #82

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ This project's release branch is `master`. This log is written from the perspect

## 2019-06-14 - Unreleased

* Add `validationDropdown` and `validationTextArea`
* Add an extra type parameter `v` specifying the widget value type (typically `Text`) to `ValidationConfig`
* Add `mkValidationConfig` which is like `defValidationConfig` but takes the initial value
* Add `validationDropdownChangeEvent` that returns the change event of the ValidationDropdown
* Add `withLoggingMinLevel` function in `Rhyolite.Backend.Logging` which allows you to pick the fallback filter when no other filters match.
* Bump obelisk to a version that no longer uses `*Tag` classes.
* Add alternative to groundhog's `==.`, which has severe performance issues before version 0.10 (to which we can't yet upgrade). See `Rhyolite.Backend.DB.===.`.
Expand Down
163 changes: 136 additions & 27 deletions frontend/Rhyolite/Frontend/Form.hs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import Control.Monad.Except
import Data.Bifunctor
import Data.Functor.Compose
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Text (Text)
Expand Down Expand Up @@ -118,10 +119,10 @@ manageValidity validate' validator errorText renderInput = do
return v

manageValidation
:: (DomBuilder t m, MonadHold t m)
=> (Dynamic t Text -> DynValidation t e a) -- Validation
-> m (InputElement EventResult (DomBuilderSpace m) t) -- Render input
-> m (InputElement EventResult (DomBuilderSpace m) t, DynValidation t e a)
:: (DomBuilder t m, HasValue w, Value w ~ Dynamic t v, MonadHold t m)
=> (Dynamic t v -> DynValidation t e a) -- Validation
-> m w -- Render input
-> m (w, DynValidation t e a)
manageValidation validator renderInput = do
input <- renderInput
return (input, validator $ value input)
Expand All @@ -135,6 +136,9 @@ validateNonEmpty m = fromEither $ do
guardEither () $ not $ T.null txt
return txt

validateJust :: Maybe a -> Validation () a
validateJust = fromEither . maybe (Left ()) pure

validateEmail :: Text -> Validation () Text
validateEmail m = fromEither $ do
ne <- toEither $ validateNonEmpty m
Expand All @@ -150,34 +154,50 @@ validateUniqueName name otherNames = fromEither $ do
guardEither () $ not $ Set.member name otherNames
return name

data ValidationConfig t m e a = ValidationConfig
-- | Configure how to perform validation of an input widget
--
-- - `e` is the error type of the validation
-- - `a` is the result type of the validation
-- - `v` is the value used (typically `Text`) internally by the widget
data ValidationConfig t m e a v = ValidationConfig
{ _validationConfig_feedback :: Either (Dynamic t e) (Dynamic t a) -> m ()
-- ^ For displaying the error in the browser with manual styling.
, _validationConfig_errorText :: e -> Text
-- ^ For the base HTML form validation, in which errors are non-empty strings.
, _validationConfig_validation :: Dynamic t Text -> DynValidation t e a
, _validationConfig_validation :: Dynamic t v -> DynValidation t e a
-- ^ Input is always being reevaluated, including when external dynamics
-- "mixed in" with this change. But rather than pushing changes downstream,
-- downstream needed to ask for them (poll) with the 'validate' field.
, _validationConfig_initialAttributes :: Map AttributeName Text
, _validationConfig_validAttributes :: Map AttributeName Text
, _validationConfig_invalidAttributes :: Map AttributeName Text
, _validationConfig_initialValue :: Text
, _validationConfig_setValue :: Maybe (Event t Text)
, _validationConfig_initialValue :: v
, _validationConfig_setValue :: Maybe (Event t v)
, _validationConfig_validate :: Event t ()
-- ^ When to show validations and open the gate so downstream gets a new
-- result. Fresh errors is the price for fresh results.
}

defValidationConfig :: DomBuilder t m => ValidationConfig t m Text a
defValidationConfig = ValidationConfig
-- | Like mkValidationConfig but for monoidal widget values
defValidationConfig
:: (DomBuilder t m, Monoid v)
=> ValidationConfig t m Text a v
defValidationConfig = mkValidationConfig mempty

-- | Make a ValidationConfig with base values.
mkValidationConfig
:: DomBuilder t m
=> v
-- ^ Initial value to use in the widget
-> ValidationConfig t m Text a v
mkValidationConfig ini = ValidationConfig
{ _validationConfig_feedback = const blank
, _validationConfig_errorText = id
, _validationConfig_validation = const $ toDynValidation $ pure $ Left "Validation not configured"
, _validationConfig_initialAttributes = mempty
, _validationConfig_validAttributes = mempty
, _validationConfig_invalidAttributes = mempty
, _validationConfig_initialValue = ""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe require Monoid v here and add a second mkBaseValidationConfig or something without that constraint that takes an argument.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. Does it make sense to deprecate defValidationConfig in the process? Because I wonder about the value of having two functions (with non-similar names to boot).

Copy link
Collaborator

@3noch 3noch Jul 31, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mkValidationConfig seems like a better name since "base" is not in the type's name. You can't deprecate the argument-taking one because v may not be monoidal. (Unless you expect people to construct from the record directly...)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't deprecate the argument-taking one

Yea, but I meant: deprecate the other one (one that has the Monoid constraint).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't that the one that @eskimor is proposing you add? Maybe I missed something.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think not having to provide an argument for the common case is useful and would make this change less breaking (maybe not breaking at all). So I don't think we need to deprecate anything.

, _validationConfig_initialValue = ini
, _validationConfig_setValue = Nothing
, _validationConfig_validate = never
}
Expand All @@ -187,41 +207,130 @@ data ValidationInput t m e a = ValidationInput
, _validationInput_value :: DynValidation t e a
}

data ValidationTextArea t m e a = ValidationTextArea
{ _validationTextArea_input :: TextAreaElement EventResult (DomBuilderSpace m) t
, _validationTextArea_value :: DynValidation t e a
}

data ValidationDropdown t e a = ValidationDropdown
{ _validationDropdown_input :: Dropdown t (Maybe a)
, _validationDropdown_value :: DynValidation t e a
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need validation for a dropdown? Shouldn't we just only offer options that are valid?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eskimor One of the items in the dropbox can be "Select an item" (which would be the default selection). If the user has not in fact selected one of the other items, we would want to display a validation error.

The "Select an item" thing would be Nothing, and the other items Just a. So we would just use the validateJust function here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Thanks!

instance HasValue (ValidationInput t m e a) where
type Value (ValidationInput t m e a) = DynValidation t e a
value = _validationInput_value

instance HasValue (ValidationTextArea t m e a) where
type Value (ValidationTextArea t m e a) = DynValidation t e a
value = _validationTextArea_value

instance HasValue (ValidationDropdown t e a) where
type Value (ValidationDropdown t e a) = DynValidation t e a
value = _validationDropdown_value

instance Reflex t => HasDomEvent t (ValidationInput t m e a) en where
type DomEventType (ValidationInput t m e a) en = DomEventType (InputElement EventResult m t) en
domEvent en = domEvent en . _validationInput_input

instance Reflex t => HasDomEvent t (ValidationTextArea t m e a) en where
type DomEventType (ValidationTextArea t m e a) en = DomEventType (TextAreaElement EventResult m t) en
domEvent en = domEvent en . _validationTextArea_input

-- | Get the event that triggers when the dropdown selection changes
--
-- Use this function as there is no HasDomEvent instance for Reflex's Dropdown
validationDropdownChangeEvent :: Reflex t => ValidationDropdown t e a -> Event t ()
validationDropdownChangeEvent = void . _dropdown_change . _validationDropdown_input

validationInput
:: (DomBuilder t m, PostBuild t m, MonadFix m, MonadHold t m)
=> ValidationConfig t m e a
:: (DomBuilder t m, PostBuild t m, MonadFix m, MonadHold t m, Semigroup e)
=> ValidationConfig t m e a Text
-> m (ValidationInput t m e a)
validationInput config = do
(vi, feedback) <- validationInputWithFeedback config
feedback
return vi

validationTextArea
:: (DomBuilder t m, PostBuild t m, MonadFix m, MonadHold t m, Semigroup e)
=> ValidationConfig t m e a Text
-> m (ValidationTextArea t m e a)
validationTextArea config = do
(vi, feedback) <- validationTextAreaWithFeedback config
feedback
return vi

validationDropdown
:: (DomBuilder t m, PostBuild t m, MonadFix m, MonadHold t m, Semigroup e, Ord a)
=> Maybe a
-> Dynamic t (Map (Maybe a) Text)
-> ValidationConfig t m e a (Maybe a)
-> m (ValidationDropdown t e a)
validationDropdown k0 options config = do
(vi, feedback) <- validationDropdownWithFeedback k0 options config
feedback
return vi

validationInputWithFeedback
:: (DomBuilder t m, PostBuild t m, MonadFix m, MonadHold t m)
=> ValidationConfig t m e a
:: ( DomBuilder t m, PostBuild t m, MonadFix m, MonadHold t m
, Semigroup e , Reflex t
)
=> ValidationConfig t m e a Text
-> m (ValidationInput t m e a, m ())
validationInputWithFeedback config = do
let validation' = _validationConfig_validate config
rec (input, dValidated) <- manageValidation (_validationConfig_validation config) $ do
inputElement $ def
& initialAttributes .~ _validationConfig_initialAttributes config
& modifyAttributes .~ inputAttrs
& inputElementConfig_initialValue .~ _validationConfig_initialValue config
& inputElementConfig_setValue %~ maybe id const (_validationConfig_setValue config)
let eValidated = tagPromptlyDyn (fromDynValidation dValidated) validation'
validationInputWithFeedback config = validationCustomInputWithFeedback config ValidationInput $ \inputAttrs ->
inputElement $ def
& initialAttributes .~ _validationConfig_initialAttributes config
& modifyAttributes .~ (fmap Just <$> inputAttrs)
& inputElementConfig_initialValue .~ _validationConfig_initialValue config
& inputElementConfig_setValue %~ maybe id const (_validationConfig_setValue config)

validationTextAreaWithFeedback
:: ( DomBuilder t m, PostBuild t m, MonadFix m, MonadHold t m
, Semigroup e , Reflex t
)
=> ValidationConfig t m e a Text
-> m (ValidationTextArea t m e a, m ())
validationTextAreaWithFeedback config = validationCustomInputWithFeedback config ValidationTextArea $ \inputAttrs ->
textAreaElement $ def
& initialAttributes .~ _validationConfig_initialAttributes config
& modifyAttributes .~ (fmap Just <$> inputAttrs)
& textAreaElementConfig_initialValue .~ _validationConfig_initialValue config
& textAreaElementConfig_setValue %~ maybe id const (_validationConfig_setValue config)

validationDropdownWithFeedback
:: ( DomBuilder t m, PostBuild t m, MonadFix m, MonadHold t m
, Semigroup e, Reflex t, Ord a
)
=> Maybe a
-> Dynamic t (Map (Maybe a) Text)
-> ValidationConfig t m e a (Maybe a)
-> m (ValidationDropdown t e a, m ())
validationDropdownWithFeedback k0 options config = validationCustomInputWithFeedback config ValidationDropdown $
\inputAttrs -> do
attrs <- holdDyn (_validationConfig_initialAttributes config) inputAttrs
let attrs' = Map.mapKeysMonotonic (\(AttributeName _ v) -> v) <$> attrs
dropdown k0 options $ def
& dropdownConfig_attributes .~ attrs'
& dropdownConfig_setValue %~ maybe id const (_validationConfig_setValue config)

validationCustomInputWithFeedback
:: ( DomBuilder t m, PostBuild t m, MonadFix m, MonadHold t m
, Semigroup e, Reflex t, HasValue w, Value w ~ Dynamic t v
)
=> ValidationConfig t m e a v
-> (w -> DynValidation t e a -> vi)
-> (Event t (Map AttributeName Text) -> m w)
-> m (vi, m ())
validationCustomInputWithFeedback config mkVi w = do
let validate' = _validationConfig_validate config
rec (input, dValidated) <- manageValidation (_validationConfig_validation config) $ w inputAttrs
let eValidated = tagPromptlyDyn (fromDynValidation dValidated) validate'
inputAttrs = ffor eValidated $ \case
Left _ -> fmap Just $ _validationConfig_invalidAttributes config
Right _ -> fmap Just $ _validationConfig_validAttributes config
Left _ -> _validationConfig_invalidAttributes config
Right _ -> _validationConfig_validAttributes config
val <- eitherDyn $ fromDynValidation dValidated
let feedback = dyn_ $ _validationConfig_feedback config <$> val
return $ (ValidationInput input dValidated, feedback)
return (mkVi input dValidated, feedback)

makeLenses ''ValidationConfig