Build composable forms comprised of fields.
This is a port of hecrj/composable-form. LICENSE located at ./src/Ant/Form/LICENSE.
Base.Form values output (Field values)
A Form
collects and validates user input using fields. When a form is filled with values
,
it produces some output
if validation succeeds.
For example, a Form String EmailAddress
is a form that is filled with a String
and produces
an EmailAddress
when valid. This form could very well be an emailField
!
A Form
is only the definition of your form logic! It only represents the shape of a form! It lives on its own, decoupled from its values, the rendering strategy and view state.
If you like to learn by example, you can check out this excellent introduction to the package by Alex Korban.
inputField : { parser : String -> Result String output, value : values -> String, update : String -> values -> values, error : values -> Maybe String, attributes : InputField.Attributes } -> Form values output
Create a form that contains a single text field.
It requires some configuration:
parser
specifies how to validate the field. It needs a function that processes the value of
the field and produces a Result
of either:String
describing an erroroutput
value
describes how to obtain the field value from the form values
update
describes how the current form values
should be updated with a new field valueattributes
let you define the specific attributes of the field (label
and placeholder
in this case, see InputField.Attributes
)It might seem like a lot of configuration, but don't be scared! In practice, it isn't!
For instance, you could use this function to build a nameField
that only succeeds when the
inputted name has at least 2 characters, like this:
nameField : Form { r | name : String } String
nameField =
Form.inputField
{ parser =
\name ->
if String.length name < 2 then
Err "the name must have at least 2 characters"
else
Ok name
, value = .name
, update =
\newValue values ->
{ values | name = newValue }
, attributes =
{ label = "Name"
, placeholder = "Type your name..."
}
}
As you can see:
parser
is just a simple validation functionvalue
using record accessorsupdate
function updates the values
of the form with the newValue
attributes
are most of the time a simple recordpasswordField : { parser : PasswordFieldValue -> Result String output, value : values -> PasswordFieldValue, update : PasswordFieldValue -> values -> values, error : values -> Maybe String, attributes : PasswordField.Attributes } -> Form values output
Create a form that contains a single password field.
It has the same configuration options as inputField
.
checkboxField : { parser : Basics.Bool -> Result String output, value : values -> Basics.Bool, update : Basics.Bool -> values -> values, error : values -> Maybe String, attributes : CheckboxField.Attributes } -> Form values output
Create a form that contains a single checkbox field.
It has a very similar configuration to inputField
, the only differences are:
Bool
instead of String
.CheckboxField.Attributes
instead of InputField.Attributes
.withAdjacentHtml : Html Basics.Never -> Form values output -> Form values output
Add arbitrary Html to a field.
Use this only on individual fields, not on an entire composed form. See example below of correct usage:
rememberMeCheckbox =
Form.checkboxField
{ parser = Ok
, value = .rememberMe
, update = \value values -> { values | rememberMe = value }
, error = always Nothing
, attributes =
{ label = "Remember me" }
}
|> Form.withAdjacentHtml (Html.a [ A.style "cursor" "pointer" ] [ Html.text "forgot password?" ])
All the functions in the previous section produce a Form
with a single
field. You might then be wondering: "How do I create a Form
with multiple fields?!"
Well, as the name of this package says: Form
is composable! This section explains how you
can combine different forms into bigger and more complex ones.
succeed : output -> Form values output
Create an empty form that always succeeds when filled, returning the given output
.
It might seem pointless on its own, but it becomes useful when used in combination with other
functions. The docs for append
have some great examples.
append : Form values a -> Form values (a -> b) -> Form values b
Append a form to another one while capturing the output
of the first one.
For instance, we could build a signup form:
signupEmailField : Form { r | email : String } EmailAddress
signupEmailField =
Form.emailField
{ -- ...
}
signupPasswordField : Form { r | password : String } Password
signupPasswordField =
Form.passwordField
{ -- ...
}
signupForm :
Form
{ email : String
, password : String
}
( EmailAddress, Password )
signupForm =
Form.succeed Tuple.pair
|> Form.append signupEmailField
|> Form.append signupPasswordField
In this pipeline, append
is being used to feed the Tuple.pair
function and combine two forms
into a bigger form that outputs ( EmailAddress, Password )
when submitted.
Note: You can use succeed
smartly to skip some values.
This is useful when you want to append some fields in your form to perform validation, but
you do not care about the output
they produce. An example of this is a "repeat password" field:
passwordForm :
Form
{ password : String
, repeatPassword : String
}
Password
passwordForm =
Form.succeed (\password repeatedPassword -> password)
|> Form.append passwordField
|> Form.append repeatPasswordField
optional : Form values output -> Form values (Maybe output)
Make a form optional. An optional form succeeds when:
Nothing
Just
the output
Let's say we want to optionally ask for a website name and address:
websiteForm =
Form.optional
(Form.succeed Website
|> Form.append websiteNameField
|> Form.append websiteAddressField
)
This websiteForm
will only be valid if both fields are blank, or both fields
are filled correctly.
disable : Form values output -> Form values output
Disable a form.
You can combine this with meta
to disable parts of a form based on its
own values.
group : Form values output -> Form values output
Render a group of fields horizontally.
Using this function does not affect the behavior of the form in any way. It is simply to change the layout of a set of fields.
section : String -> Form values output -> Form values output
Wraps a form in a section: an area with a title.
Like group
, this function has no effect on form behavior. It just
indicates to the form view function that the fields are part of some user-defined
section.
andThen : (a -> Form values b) -> Form values a -> Form values b
Fill a form andThen
fill another one.
This is useful to build dynamic forms. For instance, you could use the output of a selectField
to choose between different forms, like this:
type Msg
= CreatePost Post.Body
| CreateQuestion Question.Title Question.Body
type ContentType
= Post
| Question
type alias Values =
{ type_ : String
, title : String
, body : String
}
contentForm : Form Values Msg
contentForm =
Form.selectField
{ parser =
\value ->
case value of
"post" ->
Ok Post
"question" ->
Ok Question
_ ->
Err "invalid content type"
, value = .type_
, update = \newValue values -> { values | type_ = newValue }
, attributes =
{ label = "Which type of content do you want to create?"
, placeholder = "Choose a type of content"
, options = [ ( "post", "Post" ), ( "question", "Question" ) ]
}
}
|> Form.andThen
(\contentType ->
case contentType of
Post ->
let
bodyField =
Form.textareaField
{ -- ...
}
in
Form.succeed CreatePost
|> Form.append bodyField
Question ->
let
titleField =
Form.inputField
{ -- ...
}
bodyField =
Form.textareaField
{ -- ...
}
in
Form.succeed CreateQuestion
|> Form.append titleField
|> Form.append bodyField
)
meta : (values -> Form values output) -> Form values output
Build a form that depends on its own values
.
This is useful when you need some fields to perform validation based on the values of other fields. An example of this is a "repeat password" field:
repeatPasswordField :
Form
{ r
| password : String
, repeatPassword : String
}
()
repeatPasswordField =
Form.meta
(\values ->
Form.passwordField
{ parser =
\value ->
if value == values.password then
Ok ()
else
Err "the passwords do not match"
, value = .repeatPassword
, update =
\newValue values ->
{ values | repeatPassword = newValue }
, attributes =
{ label = "Repeat password"
, placeholder = "Type your password again..."
}
}
)
list : Base.FormList.Config values elementValues -> (Basics.Int -> Form elementValues output) -> Form values (List output)
Build a variable list of forms.
For instance, you can build a form that asks for a variable number of websites:
type alias WebsiteValues =
{ name : String
, address : String
}
websiteForm : Int -> Form WebsiteValues Website
websitesForm : Form { r | websites : List WebsiteValues } (List Website)
websitesForm =
Form.list
{ default =
{ name = ""
, address = "https://"
}
, value = .websites
, update = \value values -> { values | websites = value }
, attributes =
{ label = "Websites"
, add = Just "Add website"
, delete = Just ""
}
}
websiteForm
map : (a -> b) -> Form values a -> Form values b
Transform the output
of a form.
This function can help you to keep forms decoupled from specific view messages:
Form.map SignUp signupForm
mapValues : { value : a -> b, update : b -> a -> a } -> Form b output -> Form a output
Transform the values
of a form.
This can be useful when you need to nest forms:
type alias SignupValues =
{ email : String
, password : String
, address : AddressValues
}
addressForm : Form AddressValues Address
signupForm : Form SignupValues Msg
signupForm =
Form.succeed SignUp
|> Form.append emailField
|> Form.append passwordField
|> Form.append
(Form.mapValues
{ value = .address
, update = \newAddress values -> { values | address = newAddress }
}
addressForm
)
This section describes how to fill a Form
with its values
and obtain its
different fields and its output
. This is mostly used to write custom view code.
If you just want to render a simple form as Html
, check Form.View
first as it
Represents a form field.
If you are writing custom view code you will probably need to pattern match this type,
using the result of fill
.
Represents a type of text field
Base.FilledField (Field values)
Represents a filled field.
fill : Form values output -> values -> { fields : List (FilledField values), result : Result ( Error, List Error ) output, isEmpty : Basics.Bool }
Fill a form with some values
.
It returns:
output