Skip to content

Commit

Permalink
Add submitPreventDefault to Formless when using forms (#80)
Browse files Browse the repository at this point in the history
Co-authored-by: Thomas Honeyman <admin@thomashoneyman.com>
  • Loading branch information
chiroptical and thomashoneyman authored Nov 18, 2021
1 parent 3f6ffef commit 657225e
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 6 deletions.
2 changes: 2 additions & 0 deletions example/Main.purs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Effect (Effect)
import Effect.Aff (Aff)
import Example.Basic.Component as Basic
import Example.Async.Component as Async
import Example.Readme.Component as Readme
import Example.Nested.Page as Nested
import Example.ExternalComponents.Page as ExternalComponents
import Example.App.Home as Home
Expand All @@ -25,6 +26,7 @@ stories = Object.fromFoldable
, Tuple "async" $ proxy Async.component
, Tuple "nested" $ proxy Nested.component
, Tuple "real-world" $ proxy RealWorld.component
, Tuple "readme" $ proxy Readme.component
]

main :: Effect Unit
Expand Down
112 changes: 112 additions & 0 deletions example/readme/Component.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
-- | This example component shows how submitPreventDefault works in Halogen Formless
module Example.Readme.Component (component) where

import Prelude

import Data.Either (Either(..))
import Data.Int as Int
import Data.Maybe (Maybe(..))
import Data.Newtype (class Newtype, unwrap)
import Effect.Aff.Class (class MonadAff)
import Effect.Class.Console (logShow)
import Example.App.UI.Element as UI
import Example.App.Validation (class ToText)
import Formless as F
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP
import Type.Proxy (Proxy(..))

type Dog = { name :: String, age :: Age }

newtype Age = Age Int

derive instance newtypeAge :: Newtype Age _

instance showAge :: Show Age where
show = show <<< unwrap

data AgeError = TooLow | TooHigh | InvalidInt

newtype DogForm (r :: Row Type -> Type) f = DogForm (r
-- error input output
( name :: f Void String String
, age :: f AgeError String Age
))

derive instance newtypeDogForm :: Newtype (DogForm r f) _

instance ToText AgeError where
toText = case _ of
InvalidInt -> "Age must be an integer"
TooLow -> "Age cannot be negative"
TooHigh -> "No dog has lived past 30 before"

input :: forall m. Monad m => F.Input' DogForm m
input =
{ initialInputs: Nothing -- same as: Just (F.wrapInputFields { name: "", age: "" })
, validators: DogForm
{ name: F.noValidation
, age: F.hoistFnE_ \str -> case Int.fromString str of
Nothing -> Left InvalidInt
Just n
| n < 0 -> Left TooLow
| n > 30 -> Left TooHigh
| otherwise -> Right (Age n)
}
}

spec :: forall input m. Monad m => F.Spec' DogForm Dog input m
spec = F.defaultSpec { render = render, handleEvent = F.raiseResult }
where
render st@{ form } =
UI.formContent_
[ HH.form
[ -- Using a form forces us to deal with an event. Using '\_ -> F.submit' here
-- would fire the event and cause the page to reload. Instead, we use
-- 'F.submitPreventDefault' to avoid firing the event unnecessarily
HE.onSubmit F.submitPreventDefault
]
[ UI.input
{ label: "Name"
, help: Right "Write your dog's name"
, placeholder: "Mila"
}
[ HP.value $ F.getInput _name st.form
, HE.onValueInput (F.setValidate _name)
]
, UI.input
{ label: "Age"
, help: UI.resultToHelp "Write your dog's age" $ F.getResult _age st.form
, placeholder: "3"
}
[ HP.value $ F.getInput _age form
, HE.onValueInput $ F.setValidate _age
]
, UI.buttonPrimary
[]
[ HH.text "Submit" ]
]
]
where
_name = Proxy :: Proxy "name"
_age = Proxy :: Proxy "age"

data Action = HandleDogForm Dog

component :: forall q i o m. MonadAff m => H.Component q i o m
component = H.mkComponent
{ initialState: const unit
, render: const render
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
}
where
handleAction (HandleDogForm dog) = logShow (dog :: Dog)

render =
UI.section_
[ UI.h1_ [ HH.text "Formless" ]
, UI.h2_ [ HH.text "The form from the readme" ]
, HH.slot F._formless unit (F.component (const input) spec) unit HandleDogForm
]
7 changes: 4 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ spec :: forall input m. Monad m => F.Spec' DogForm Dog input m
spec = F.defaultSpec { render = render, handleEvent = F.raiseResult }
where
render st@{ form } =
HH.form_
HH.form
[ HE.onSubmit F.submitPreventDefault
]
[ HH.input
[ HP.value $ F.getInput _name form
, HE.onValueInput $ F.set _name
Expand All @@ -105,8 +107,7 @@ spec = F.defaultSpec { render = render, handleEvent = F.raiseResult }
Just InvalidInt -> "Age must be an integer"
Just TooLow -> "Age cannot be negative"
Just TooHigh -> "No dog has lived past 30 before"
, HH.button
[ HE.onClick \_ -> F.submit ]
, HH.button_
[ HH.text "Submit" ]
]
where
Expand Down
1 change: 1 addition & 0 deletions spago.dhall
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
, "typelevel-prelude"
, "unsafe-coerce"
, "variant"
, "web-events"
]
, packages = ./packages.dhall
, sources = [ "src/**/*.purs" ]
Expand Down
2 changes: 1 addition & 1 deletion src/Formless.purs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ module Formless
, module Formless.Validation
) where

import Formless.Action (asyncModifyValidate, asyncSetValidate, injAction, loadForm, modify, modifyAll, modifyValidate, modifyValidateAll, reset, resetAll, set, setAll, setValidate, setValidateAll, submit, validate, validateAll)
import Formless.Action (asyncModifyValidate, asyncSetValidate, injAction, loadForm, modify, modifyAll, modifyValidate, modifyValidateAll, reset, resetAll, set, setAll, setValidate, setValidateAll, submit, submitPreventDefault, validate, validateAll)
import Formless.Class.Initial (class Initial, initial)
import Formless.Component (component, defaultSpec, handleAction, handleQuery, raiseResult)
import Formless.Data.FormFieldResult (FormFieldResult(..), _Error, _Success, fromEither, toMaybe)
Expand Down
20 changes: 18 additions & 2 deletions src/Formless/Action.purs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import Formless.Types.Form (InputField, InputFunction, U(..))
import Heterogeneous.Mapping as HM
import Prim.Row as Row
import Type.Proxy (Proxy(..))
import Web.Event.Event as Event

-- | Inject your own action into the Formless component so it can be used in HTML
injAction :: forall form act. act -> Action form act
Expand Down Expand Up @@ -264,8 +265,9 @@ resetAll :: forall v. Variant (resetAll :: Unit | v)
resetAll =
inj (Proxy :: _ "resetAll") unit

-- | Submit the form, which will trigger a `Submitted` result if the
-- | form validates successfully.
-- | Submit the form, which will trigger a `Submitted` result if the form
-- | validates successfully. If you want to capture the form submission event
-- | and submit your form use `submitPreventDefault`.
-- |
-- | ```purescript
-- | [ HE.onClick \_ -> Just F.submit ]
Expand All @@ -274,6 +276,20 @@ submit :: forall v. Variant (submit :: Unit | v)
submit =
inj (Proxy :: _ "submit") unit

-- | Submit the form, calling `preventDefault` from `Web.Event.Event` on the
-- | submission event to prevent the browser from refreshing the page.
-- |
-- | ```purescript
-- | HH.form
-- | [ HE.onSubmit F.submitPreventDefault ]
-- | [ ... ]
-- | ```
submitPreventDefault
:: forall v
. Event.Event
-> Variant (submitPreventDefault :: Event.Event | v)
submitPreventDefault = inj (Proxy :: _ "submitPreventDefault")

-- | Load a form from a set of existing inputs. Useful for when you need to mount
-- | Formless, perform some other actions like request data from the server, and
-- | then load an existing set of inputs.
Expand Down
5 changes: 5 additions & 0 deletions src/Formless/Component.purs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import Prim.RowList as RL
import Record.Builder as Builder
import Type.Proxy (Proxy(..))
import Unsafe.Coerce (unsafeCoerce)
import Web.Event.Event as Event

-- | The default spec, which can be overridden by whatever functions you need
-- | to extend the component. For example:
Expand Down Expand Up @@ -321,6 +322,10 @@ handleAction handleAction' handleEvent action = flip match action
_ <- handleAction handleAction' handleEvent FA.validateAll
IC.submit >>= traverse_ (Submitted >>> handleEvent)

, submitPreventDefault: \event -> do
H.liftEffect $ Event.preventDefault event
handleAction handleAction' handleEvent FA.submit

, loadForm: \formInputs -> do
let setFields rec = rec { allTouched = false, initialInputs = formInputs }
st <- H.get
Expand Down
2 changes: 2 additions & 0 deletions src/Formless/Types/Component.purs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import Halogen.Query.ChildQuery (ChildQueryBox)
import Halogen.Query.HalogenM (ForkId)
import Type.Proxy (Proxy(..))
import Type.Row (type (+))
import Web.Event.Event as Event

-- | A type representing the various functions that can be provided to extend
-- | the Formless component. Usually only the `render` function is required,
Expand Down Expand Up @@ -62,6 +63,7 @@ type PublicAction form =
, validateAll :: Unit
, resetAll :: Unit
, submit :: Unit
, submitPreventDefault :: Event.Event
, loadForm :: form Record InputField
)

Expand Down

0 comments on commit 657225e

Please sign in to comment.