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

Impure validation #67

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
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
62 changes: 43 additions & 19 deletions frontend/Rhyolite/Frontend/Form.hs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Rhyolite.Frontend.Form where

import Control.Lens ((%~), makeLenses, preview)
Expand Down Expand Up @@ -101,7 +103,7 @@ manageValidity
, Prerender js t m, RawInputElement (DomBuilderSpace m) ~ HTMLInputElement
)
=> Event t () -- When to validate
-> (Dynamic t Text -> DynValidation t e a) -- Validation
-> (Dynamic t Text -> m (DynValidation t e a)) -- Validation
-> (e -> Text) -- convert error to form for basic html validation
-> m (InputElement EventResult (DomBuilderSpace m) t) -- Render input
-> m (InputElement EventResult (DomBuilderSpace m) t, DynValidation t e a)
Expand All @@ -119,12 +121,13 @@ manageValidity validate' validator errorText renderInput = do

manageValidation
:: (DomBuilder t m, MonadHold t m)
=> (Dynamic t Text -> DynValidation t e a) -- Validation
=> (Dynamic t Text -> m (DynValidation t e a)) -- Validation
-> m (InputElement EventResult (DomBuilderSpace m) t) -- Render input
-> m (InputElement EventResult (DomBuilderSpace m) t, DynValidation t e a)
manageValidation validator renderInput = do
input <- renderInput
return (input, validator $ value input)
validated <- validator $ value input
return (input, validated)

guardEither :: e -> Bool -> Either e ()
guardEither e cond = if cond then Right () else Left e
Expand Down Expand Up @@ -159,6 +162,10 @@ data ValidationConfig t m e a = ValidationConfig
-- ^ 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_validationM :: Maybe (Dynamic t Text -> m (DynValidation t e a))
-- ^ This validation allows for the use of monadic effects (e.g. ask a
-- server). The results of `_validationConfig_validatation` and
-- `_validationConfig_validationM` will be combined by `*>`.
, _validationConfig_initialAttributes :: Map AttributeName Text
, _validationConfig_validAttributes :: Map AttributeName Text
, _validationConfig_invalidAttributes :: Map AttributeName Text
Expand All @@ -174,6 +181,7 @@ defValidationConfig = ValidationConfig
{ _validationConfig_feedback = const blank
, _validationConfig_errorText = id
, _validationConfig_validation = const $ toDynValidation $ pure $ Left "Validation not configured"
, _validationConfig_validationM = Nothing
, _validationConfig_initialAttributes = mempty
, _validationConfig_validAttributes = mempty
, _validationConfig_invalidAttributes = mempty
Expand All @@ -196,7 +204,7 @@ instance Reflex t => HasDomEvent t (ValidationInput t m e a) en where
domEvent en = domEvent en . _validationInput_input

validationInput
:: (DomBuilder t m, PostBuild t m, MonadFix m, MonadHold t m)
:: (DomBuilder t m, PostBuild t m, MonadFix m, MonadHold t m, Semigroup e)
=> ValidationConfig t m e a
-> m (ValidationInput t m e a)
validationInput config = do
Expand All @@ -205,23 +213,39 @@ validationInput config = do
return vi

validationInputWithFeedback
:: (DomBuilder t m, PostBuild t m, MonadFix m, MonadHold t m)
:: forall t m e a
. ( DomBuilder t m, PostBuild t m, MonadFix m, MonadHold t m
, Semigroup e , Reflex t
)
=> ValidationConfig t m e a
-> 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'
inputAttrs = ffor eValidated $ \case
Left _ -> fmap Just $ _validationConfig_invalidAttributes config
Right _ -> fmap Just $ _validationConfig_validAttributes config
val <- eitherDyn $ fromDynValidation dValidated
let feedback = dyn_ $ _validationConfig_feedback config <$> val
return $ (ValidationInput input dValidated, feedback)
let validateL = _validationConfig_validate config
validationL = combineValidators
(_validationConfig_validation config) (_validationConfig_validationM config)
rec (input, dValidated) <- manageValidation validationL $ 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) validateL
inputAttrs = ffor eValidated $ \case
Left _ -> fmap Just $ _validationConfig_invalidAttributes config
Right _ -> fmap Just $ _validationConfig_validAttributes config
val <- eitherDyn $ fromDynValidation dValidated
let feedback = dyn_ $ _validationConfig_feedback config <$> val
return $ (ValidationInput input dValidated, feedback)
where
combineValidators
:: (Dynamic t Text -> DynValidation t e a)
-> Maybe (Dynamic t Text -> m (DynValidation t e a))
-> Dynamic t Text -> m (DynValidation t e a)
combineValidators pValidator mValidator t =
case mValidator of
Nothing -> pure $ pValidator t
Just mv -> do
r <- mv t
pure (pValidator t *> r)

makeLenses ''ValidationConfig