supermacro / elm-antd / Ant.Form

Build composable forms comprised of fields.

This is a port of hecrj/composable-form. LICENSE located at ./src/Ant/Form/LICENSE.

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

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:

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:

passwordField : { 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:

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?" ])

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

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

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


type Field values
    = Text TextType (InputField values)
    | Password (PasswordField values)
    | Number (Base.NumberField.NumberField Basics.Float values)
    | Range (Base.RangeField.RangeField Basics.Float values)
    | Checkbox (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
    | TextArea ({ rows : Basics.Int })

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: