/purescript-halogen-formless

A renderless component to build forms in Halogen

Primary LanguagePureScriptApache License 2.0Apache-2.0

Formless

CircleCI Latest release Latest package set Maintainer: thomashoneyman

Formless is a flexible, extensible, type-safe Halogen component for building forms without boilerplate.

You're viewing the readme for the upcoming 1.0 release, based on Halogen 5. If you're using Halogen 4, you should browse the v0.5.2 release instead.

Quick Start

You can write a basic Formless form in just a few lines of code. You are responsible for providing just a few pieces of information.

First, a form type that describes the fields in your form, along with their validation error type, user input type, and validated output type. Note: you can provide whatever custom error types you'd like, use Void to represent no possible errors, parse to whatever type you want, and none of your fields need to share any types.

import Prelude
import Data.Newtype (class Newtype, unwrap)

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 f = DogForm (r
  --          error    input  output
  ( name :: f Void     String String
  , age  :: f AgeError String Age
  ))

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

Next, the component input, which is made up of initial values and validation functions for each field in your form. Note: with your form type complete, the compiler will verify that your inputs are of the right type, that your validation takes the right input type, produces the right error type, and parses to the right output type, that fields exist at these proper keys, and more. There's no loss in type safety here! Plus, your validation functions can easily reference the value of other fields in the form, perform monadic effects, get debounced before running, and more.

You can generate sensible defaults for all input fields in your form by setting initialInputs to Nothing, or you can manually provide the starting value for each field in your form.

import Data.Either (Either(..))
import Data.Int as Int
import Data.Maybe (Maybe(..))
import Formless as F

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)
      }
  }

Finally, the component spec, which is made up of a number of optional functions and types you can use to extend the Formless component. At minimum you will need to provide your own render function that describes how your form should be presented to the user. But you can also freely extend the Formless state, query, action, child slots, and message types, as well as provide your own handlers for your extended queries, actions, and child slots, and handle Formless messages internally without leaking information to a parent. You can extend Formless to an incredible degree -- or you can keep things simple and just provide render function. All extensions are optional.

For our small form, we'll do two things: we'll provide a render function, and when the form is submitted, we'll output a Dog to parent components. Along the way we'll wire things up so that input fields display their current value from form state; typing into an input field updates its value in state, also running the correct validation function; we'll display the validation error for age if there is one; and we'll wire up a submit button.

Note: If you would like to have your form raise no messages (rare), do not supply a handleEvent function. If you would like to raise the usual Formless messages (Changed, Submitted), then provide H.raise as your handleEvent function. If you would like to simply raise your form's validated output type (Dog, in this example), then provide F.raiseResult as your handleEvent function. Finally, if you want to do something else, you can write a custom function that does whatever you would like.

import Data.Symbol (SProxy(..))
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP

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.input
          [ HP.value $ F.getInput _name form
          , HE.onValueInput $ Just <<< F.set _name
          ]
      , HH.input
          [ HP.value $ F.getInput _age form
          , HE.onValueInput $ Just <<< F.setValidate _age
          ]
      , HH.text case F.getError _age form of
          Nothing -> ""
          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 \_ -> Just F.submit ]
          [ HH.text "Submit" ]
      ]
    where
    _name = SProxy :: SProxy "name"
    _age = SProxy :: SProxy "age"

Our form is now complete. It's easy to put this form in a parent page component:

import Effect.Aff.Class (class MonadAff)
import Effect.Class.Console (logShow)
import Halogen as H

data Action = HandleDogForm Dog

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

  render = HH.slot F._formless unit (F.component (const input) spec) unit handler
    where
    handler = Just <<< HandleDogForm

Next Steps

Ready to move past this simple example? Check out the examples, which vary in their complexity:

Running the examples locally

If you'd like to explore the example forms locally, you can run them by cloning this repository and then running these commands in the root of the project:

npm i
npm run watch-all

Open a second console to start a webserver.

pulp server

The examples will be live at http://localhost:1337/dist/. You can also skip the webserver and simply open the index.html file in your web browser directly.

Comments & Improvements

Have any comments about the library or any ideas to improve it for your use case? Please file an issue or reach out on the PureScript user group.