Giter Club home page Giter Club logo

halogen-form's Introduction

halogen-form

This implements formlets as in Cooper, Lindley, Wadler and Yallop's paper The Essence of Form Abstraction for the halogen package.

Introduction

The component

Halogen.Form.component provides a Halogen component to put a form into your HTML. Its type is:

component ::
   forall error value m.
   H.Component
     HH.HTML
     Query
     (FormBuilder error (Array (H.ComponentHTML Query)) value) -- Input
     (Either (Array error) value) -- Output
     m

Form builders

The input to the component is a FormBuilder, which looks like this:

newtype FormBuilder error html value
  • The error type is user-defined. We'll see how below.
  • The html type is user-defined.
  • The value that the form produces in the end.

Underneath the FormBuilder API there is an internal type, which is produced by a FormBuilder, which is the Form type:

data Form error html value = Form
  { value :: Submitted -> Map Int String -> Either (Array error) value
  , html :: Submitted -> Map Int String -> html
  }

A form simply has a value and a way to render it. The Map Int String associates form inputs with their values, if any. A given Form knows what Int key (provided by the FormBuilder) to use to pull a value or many values from the input.

The simplest form builders

The most basic form builder would be Form.text which has this type:

text ::
     forall a e.
  -> Maybe String
  -> FormBuilder e (Array (HH.HTML a (Query Unit))) (Maybe String)

The Maybe String is the default input, if any.

Another is number, which is the HTML5 number input:

number ::
     forall e a.
     Maybe Number
  -> FormBuilder e (Array (HH.HTML a (Query Unit))) (Maybe Number)

Defining errors for your form

A text input's value may be missing, we might want to make them required to turn that Maybe String into a String; so we provide a record telling the builder which error constructor from our error type e to use. It looks like this:

data FormError
  = MissingInput
  -- Etc.

errors :: { missing :: FormError}
errors = {missing: MissingInput}

And then you can use required:

required ::
     forall e r a html.
     {missing :: e | r}
  -> FormBuilder e html (Maybe a)
  -> FormBuilder e html a

As Form.required errors (Form.text Nothing).

Elsewhere in the app, you'll have a printing function:

printFormError msg =
  HH.strong_
    [ HH.text
        (case msg of
           MissingInput -> "Please fill everything in."
    ]

Which lets you use your own way of talking to explain error messages.

Using the form component in a slot

With our error type defined, we can use the component and build a form:

data Slot = FormSlot
derive instance eqButtonSlot :: Eq Slot
derive instance ordButtonSlot :: Ord Slot

HH.slot FormSlot Form.component (Form.required errors (Form.text Nothing)) (\value -> Nothing)

(Halogen.Form is imported as Form.)

This form will produce a String in the value given to the output handler. In that output handler you can send the form value to your eval function as usual.

Combining form builders

We can combine form builders together with Applicative:

HH.slot
  FormSlot
  Form.component
  (Tuple <$> Form.required (Form.text errors Nothing)
         <*> Form.required (Form.number errors Nothing)
         <*  Form.submitInput "Submit!")
  (\value -> Nothing)

Building records

With the (<|*>) combinator that sits in place of <*>, you can build a record instead:

HH.slot
  FormSlot
  Form.component
  (     map {name: _} (Form.required (Form.text errors Nothing))
   <|*> map {age: _} (Form.required (Form.number errors Nothing))
   <*   Form.submitInput "Submit!")
  (\value -> Nothing)

And now your value will be a record of type

{name :: String, age :: Number}

E.g.

person ::
  forall h.
    FormBuilder
      FormError
      (Array (HH.HTML h (Query Unit)))
      { name :: String, age :: Number}
person =
  map {name: _} (Form.required (Form.text errors Nothing)) <|*>
  map {age: _} (Form.required (Form.number errors Nothing)) <*
  submitInput "Submit!"

Validation

We can add validation to this form using the parse combinator:

parse ::
     forall a b h e.
     (a -> Either (Array e) b)
  -> FormBuilder e h a
  -> FormBuilder e h b

For example:

person ::
  forall h.
    FormBuilder
      FormError
      (Array (HH.HTML h (Query Unit)))
      { approved :: String }
person =
  parse
    (\them ->
       if them . name == "Crocodile Hunter" || them . age > 70
         then Left [InsuranceApplicationFailed]
         else Right {approved: them . name})
    (map {name: _} (Form.text errors Nothing) <|*>
     map {age: _}
       (parse
          (\age ->
             if age > 18 && age < 100
               then Right age
               else Left [InvalidAge])
          (Form.number errors Nothing)) <*
     submitInput "Submit!")

Here I've demonstrated two things:

  1. Using parse on an individual form input to validate age.
  2. Using parse to apply a life insurance policy on multiple fields.

Composability

The fact that validation, input and rendering are all coupled means I can separate age into a re-usable component throughout my app:

ageInput ::
     forall h.
     Maybe Number
  -> FormBuilder FormError (Array (HH.HTML h (Query Unit))) Number
ageInput def =
  parse
    (\age ->
       if age > 18.0 && age < 100.0
         then Right age
         else Left [InvalidAge])
    (Form.number errors def)

Or make it even more generic to be used across different types of errors:

ageInput ::
     forall h e errors.
     {invalidAge :: e, missing :: e | errors}
  -> Maybe Number
  -> FormBuilder e (Array (HH.HTML h (Query Unit))) Number
ageInput es def =
  parse
    (\age ->
       if age > 18.0 && age < 100.0
         then Right age
         else Left [es.invalidAge])
    (Form.number es def)

Wrapping up

You can wrap your own custom HTML around other form builders using wrap:

wrap ::
     forall e a html.
     (Maybe (Array e) -> html -> html)
  -> FormBuilder e html a
  -> FormBuilder e html a

You can choose to print the error messages around an input, if you like. Otherwise you can display them in e.g. a list above.

halogen-form's People

Contributors

chrisdone avatar maxsnew avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

halogen-form's Issues

Dealing with dynamic input

I was wondering if there is a way to re-render the component when the Formbuilder is dynamically changed. Normally, the Form component would need to be a lifecycle component but the current implementation is not.

Maybe there is a way to manually call re-rendering of the child component which in this case is the Form component.

Hope to get some help on this.

Dealing with Array/List type of inputs

First of all, thanks for this great library. One thing that I have problem with is doing dynamic checkboxes. I am trying to find way to create the formbuilder so that the result it return would be:

{ name :: String
, desc :: String
, categories :: Array String
}

Another approach would be passing string as record name field so that we can do

( map {"name": _} Form.required errors (Form.text Nothing)
 <|*> map {"desc": _} Form.required errors (Form.text Nothing)
 <|*> (mapM_ (\x -> map {"x": _} Form.required errors (Form.checkbox Nothing)) ["category1", "category2"])
)

which should return

{ name :: String
, desc :: String
, category1 :: String
, category2 :: String
}

But I'm still not sure how to turn that {category1 :: String, category2 :: String} into {categories :: Array String}

Or there maybe other approach all together, hope someone shed some light on this.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.