This implements formlets as in Cooper, Lindley, Wadler and Yallop's paper The Essence of Form Abstraction for the halogen package.
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
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 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)
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.
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.
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)
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!"
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:
- Using
parse
on an individual form input to validate age. - Using
parse
to apply a life insurance policy on multiple fields.
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)
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.