diff --git a/example/Main.purs b/example/Main.purs index 3b46e51..d44f4a6 100644 --- a/example/Main.purs +++ b/example/Main.purs @@ -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 @@ -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 diff --git a/example/readme/Component.purs b/example/readme/Component.purs new file mode 100644 index 0000000..a2917db --- /dev/null +++ b/example/readme/Component.purs @@ -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 + ] diff --git a/readme.md b/readme.md index f4e2d6b..82387ee 100644 --- a/readme.md +++ b/readme.md @@ -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 @@ -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 diff --git a/spago.dhall b/spago.dhall index 20360e5..a70c5c2 100644 --- a/spago.dhall +++ b/spago.dhall @@ -24,6 +24,7 @@ , "typelevel-prelude" , "unsafe-coerce" , "variant" + , "web-events" ] , packages = ./packages.dhall , sources = [ "src/**/*.purs" ] diff --git a/src/Formless.purs b/src/Formless.purs index 9aeaa3b..4c42da5 100644 --- a/src/Formless.purs +++ b/src/Formless.purs @@ -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) diff --git a/src/Formless/Action.purs b/src/Formless/Action.purs index 5c8d441..4a629bb 100644 --- a/src/Formless/Action.purs +++ b/src/Formless/Action.purs @@ -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 @@ -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 ] @@ -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. diff --git a/src/Formless/Component.purs b/src/Formless/Component.purs index 161bcb6..b52164f 100644 --- a/src/Formless/Component.purs +++ b/src/Formless/Component.purs @@ -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: @@ -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 diff --git a/src/Formless/Types/Component.purs b/src/Formless/Types/Component.purs index 569f638..2640935 100644 --- a/src/Formless/Types/Component.purs +++ b/src/Formless/Types/Component.purs @@ -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, @@ -62,6 +63,7 @@ type PublicAction form = , validateAll :: Unit , resetAll :: Unit , submit :: Unit + , submitPreventDefault :: Event.Event , loadForm :: form Record InputField )