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 submitPreventDefault to Formless when using forms #80

Merged
merged 6 commits into from
Nov 18, 2021
Merged
Show file tree
Hide file tree
Changes from 2 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
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
121 changes: 121 additions & 0 deletions example/readme/Component.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
-- | 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 "A form using a default form element" ]
, UI.p_
"""
In formless, if you use a default form you would notice a page refresh when submitting the form.
In the other examples, this is avoided by not using a form element. In this example, we use
a special function F.submitPreventDefault to avoid the page refresh.

You can download the examples and replace F.submitPreventDefault with
\_ -> F.submit to see the difference.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The text explaining how submitPreventDefault changes the behavior of them form and how to modify the example is probably worth reviewing as well.

Copy link
Owner

Choose a reason for hiding this comment

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

I think this can be removed, probably, as you really ought to just use forms this way in general. I think it'd be better to add documentation to submit that says it can be used to submit the form imperatively, but that if you want to capture the form submission event and submit your form that you should use submitPreventDefault

Choose a reason for hiding this comment

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

We introduced submitPreventDefault to avoid the breaking change. Maybe it could be changed to work the other way around in a future major release?

Copy link
Owner

Choose a reason for hiding this comment

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

Yea, I’d rather have ‘submit’ and ‘submit_’ using the usual Halogen naming conventions in the future!

"""
, 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
]
Comment on lines +94 to +96
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Use submitPreventDefault in the readme example to avoid confusion

Copy link
Owner

Choose a reason for hiding this comment

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

I can do this in a follow up PR if you'd like, but we really should switch all forms to be written this way

Choose a reason for hiding this comment

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

I was thinking to do that follow up PR, if that's not a problem

[ 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
14 changes: 14 additions & 0 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 @@ -274,6 +275,19 @@ submit :: forall v. Variant (submit :: Unit | v)
submit =
inj (Proxy :: _ "submit") unit

-- | Submit the form, which will trigger a `Submitted` result if the form
-- | validates successfully. Calls `preventDefault` (`Web.Event.Event`) on the
-- | event
-- |
-- | ```purescript
-- | [ HE.onSubmit F.submitPreventDefault ]
-- | ```
chiroptical marked this conversation as resolved.
Show resolved Hide resolved
submitPreventDefault
:: forall v
. Event.Event
-> Variant (submitPreventDefault :: Event.Event | v)
submitPreventDefault = inj (Proxy :: _ "submitPreventDefault")
chiroptical marked this conversation as resolved.
Show resolved Hide resolved

-- | 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
7 changes: 7 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,12 @@ 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
_ <- IC.preSubmit
_ <- handleAction handleAction' handleEvent FA.validateAll
IC.submit >>= traverse_ (Submitted >>> handleEvent)
chiroptical marked this conversation as resolved.
Show resolved Hide resolved

, 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