-
Notifications
You must be signed in to change notification settings - Fork 17
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
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
|
@@ -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) | ||
|
@@ -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 | ||
|
@@ -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 = "" | ||
, _validationConfig_initialValue = ini | ||
, _validationConfig_setValue = Nothing | ||
, _validationConfig_validate = never | ||
} | ||
|
@@ -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 | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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).There was a problem hiding this comment.
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 becausev
may not be monoidal. (Unless you expect people to construct from the record directly...)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, but I meant: deprecate the other one (one that has the Monoid constraint).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.