hecrj / composable-form / Form

Build composable forms comprised of fields.

Definition


type alias Form values output =
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.

Fields

textField : { parser : String -> Result String output, value : values -> String, update : String -> values -> values, error : values -> Maybe String, attributes : Base.TextField.Attributes } -> Form values output

Create a form that contains a single text field.

It requires some configuration:

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.textField
        { 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:

emailField : { parser : String -> Result String output, value : values -> String, update : String -> values -> values, error : values -> Maybe String, attributes : Base.TextField.Attributes } -> Form values output

Create a form that contains a single email field.

It has the same configuration options as textField.

passwordField : { parser : String -> Result String output, value : values -> String, update : String -> values -> values, error : values -> Maybe String, attributes : Base.TextField.Attributes } -> Form values output

Create a form that contains a single password field.

It has the same configuration options as textField.

textareaField : { parser : String -> Result String output, value : values -> String, update : String -> values -> values, error : values -> Maybe String, attributes : Base.TextField.Attributes } -> Form values output

Create a form that contains a single textarea field.

It has the same configuration options as textField.

numberField : { parser : String -> Result String output, value : values -> String, update : String -> values -> values, error : values -> Maybe String, attributes : Base.NumberField.Attributes Basics.Float } -> Form values output

Create a form that contains a single number field.

It has a very similar configuration to textField, the only difference is:

rangeField : { parser : Maybe Basics.Float -> Result String output, value : values -> Maybe Basics.Float, update : Maybe Basics.Float -> values -> values, error : values -> Maybe String, attributes : Base.RangeField.Attributes Basics.Float } -> Form values output

Create a form that contains a single range field.

It has a very similar configuration to textField, the only difference is:

checkboxField : { parser : Basics.Bool -> Result String output, value : values -> Basics.Bool, update : Basics.Bool -> values -> values, error : values -> Maybe String, attributes : Base.CheckboxField.Attributes } -> Form values output

Create a form that contains a single checkbox field.

It has a very similar configuration to textField, the only differences are:

radioField : { parser : String -> Result String output, value : values -> String, update : String -> values -> values, error : values -> Maybe String, attributes : Base.RadioField.Attributes } -> Form values output

Create a form that contains a single fieldset of radio fields.

It has a very similar configuration to textField, the only difference is:

selectField : { parser : String -> Result String output, value : values -> String, update : String -> values -> values, error : values -> Maybe String, attributes : Base.SelectField.Attributes } -> Form values output

Create a form that contains a single select field.

It has a very similar configuration to textField, the only difference is:

Composition

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:

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

Wraps a form in a group.

Using this function does not affect the behavior of the form in any way. However, groups of fields might be rendered differently. For instance, Form.View renders groups of fields horizontally.

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.textField
                                    { -- ...
                                    }

                            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

Mapping

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
            )

Output

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 might suit your needs.


type Field values
    = Text TextType (Base.TextField.TextField values)
    | Number (Base.NumberField.NumberField Basics.Float values)
    | Range (Base.RangeField.RangeField Basics.Float values)
    | Checkbox (Base.CheckboxField.CheckboxField values)
    | Radio (Base.RadioField.RadioField values)
    | Select (Base.SelectField.SelectField values)
    | Group (List (FilledField values))
    | Section String (List (FilledField values))
    | List (Base.FormList.FormList values (Field values))

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.


type TextType
    = TextRaw
    | TextEmail
    | TextPassword
    | TextArea
    | TextSearch

Represents a type of text field


type alias FilledField values =
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: