dillonkearns/elm-form/1.0.0

NOTE: the latest version is 3.1.0

Form.Field

type alias Field error parsed input initial kind constraints =
Field error parsed input initial kind constraints

A Field represents the base information of how to turn a raw input into a parsed value, and how to display that value as an HTML form field element. Note that you can also perform more advanced validations and mapping using the Form.Validation API in your combine function.

For example, if you want to display a check-in and check-out date field, you would use date Fields. Using date does two things:

  1. Sets display options for the browser to display the field with <input type="date" />.

  2. Parses into an Elm value of type Date (or a validation error if the format isn't invalid).

Why would the date format be invalid if it is managed by the Browser's date picker element? In the happy path on the Browser this will never happen.

However, you can't make any assumptions about the client (non-standard clients could be used), or about the data format that is sent to servers. Often validation logic is duplicated to guard against this on both the client (display useful validation feedback for the user) and the server (validate untrusted input). If you use full-stack Elm, you can use your Form definition on your server to run the same code that you use to present validation errors on the client, allowing you to keep the validations in sync.

Here is an example that showcases different types of Fields (text, date, time, and checkbox), as well as how to use the combine function to transform the parsed values and perform additional validations between the parsed fields.

import Date exposing (Date)
import Form
import Form.Field as Field
import Form.Validation as Validation


type alias Stay =
    { name : String
    , checkIn : Checkin
    , emailUpdates : Bool
    }


type alias Checkin =
    { date : Date
    , nights : Int
    , time : TimeOfDay
    }


example : Form.HtmlForm String Stay input Msg
example =
    (\name checkIn checkOut checkInTime emailUpdates ->
        { combine =
            Validation.succeed Stay
                |> Validation.andMap name
                |> Validation.andMap
                    (Validation.succeed
                        (\checkinValue checkoutValue checkInTimeValue ->
                            Validation.succeed
                                { date = checkinValue
                                , nights = Date.toRataDie checkoutValue - Date.toRataDie checkinValue
                                , time = checkInTimeValue
                                }
                                |> Validation.withErrorIf (Date.toRataDie checkinValue >= Date.toRataDie checkoutValue) checkIn "Must be before checkout"
                        )
                        |> Validation.andMap checkIn
                        |> Validation.andMap checkOut
                        |> Validation.andMap checkInTime
                        |> Validation.andThen identity
                    )
                |> Validation.andMap emailUpdates
        , view =
            \formState ->
                let
                    fieldView label field =
                        Html.div []
                            [ Html.label []
                                [ Html.text (label ++ " ")
                                , FieldView.input [] field
                                , errorsView formState field
                                ]
                            ]
                in
                [ fieldView "Name" name
                , fieldView "Check-In" checkIn
                , fieldView "Check-Out" checkOut
                , fieldView "Check-In Time" checkInTime
                , fieldView "Sign Up For Email Updates" emailUpdates
                , Html.button [] [ Html.text "Submit" ]
                ]
        }
    )
        |> Form.form
        |> Form.field "name"
            (Field.text
                |> Field.required "Required"
            )
        |> Form.field "checkin"
            (Field.date
                { invalid = \_ -> "Invalid" }
                |> Field.required "Required"
                |> Field.withMin today ("Must be after " ++ Date.toIsoString today)
            )
        |> Form.field "checkout"
            (Field.date
                { invalid = \_ -> "Invalid" }
                |> Field.required "Required"
            )
        |> Form.field "checkinTime"
            (Field.time
                { invalid = \_ -> "Invalid" }
                |> Field.required "Required"
                |> Field.withMin { hours = 10, minutes = 0 } "Must be after today"
            )
        |> Form.field "emailUpdates"
            Field.checkbox


today : Date
today =
    Date.fromRataDie 738624

The key concepts to understand here are where the view and validation logic for fields lives.

The Form's view function is responsible for combining the rendered Fields, but the Field contains the information for how to display the form field itself (details like like <input type="date" />, <input type="checkbox" />, <textarea> etc.). Since form fields contain both display logic that changes how we parse/validate the field, all of the parsing and validation logic related to displaying the Field is also defined with Field type. For example, Field.withMin contains information that is used both for displaying the form field and for validating it. When we set Field.withMin, it gives the Browser information on how to restrict the date picker UI from showing invalid dates, but setting that minimum value also runs that validation when we run the Form parser, even if we run it on a server through code sharing.

Note that the validations in a Fields definition (such as Field.withMin, or Field.date, etc.) are run regardless of whether you use that field in the Form's combine function.

Base Fields

Field
error
(Maybe String)
input
String
Input
{ required : ()
, plainText : ()
, wasMapped : No
, minlength : ()
, maxlength : ()
}

The base for a text field. You can add display modifiers to text fields, including displaying them as a textarea. See Text Field Display Options.

By default, text fields are not required. If the field is not touched or has been deleted, the value will be Nothing (not empty string Just ""). See required.

import Form.Field as Field


type alias Profile =
    { status : Maybe String
    }


example =
    (\username ->
        { combine =
            Validation.succeed Status
                |> Validation.andMap username
        , view = []
        }
    )
        |> Form.form
        |> Form.field "status" Field.text
checkbox : Field error Bool input Bool Input { required : () }

Renders a checkbox input (<input type="checkbox" />), see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox.

import Form.Field as Field


example =
    Field.checkbox
int :
{ invalid : String -> error }
-> Field
error
(Maybe Int)
input
Int
Input
{ min : Int
, max : Int
, required : ()
, wasMapped : No
, step : Int
}

Renders a number input (<input type="number" />), see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/number.

Floating point numbers will give a validation error, using the error value passed in through the invalid function.

import Form.Field as Field


example =
    Field.number
        { invalid =
            \value -> "Must be an integer"
        }
{ invalid : String -> error }
-> Field
error
(Maybe Float)
input
Float
Input
{ min : Float
, max : Float
, required : ()
, wasMapped : No
}

A number input (<input type="number" />), see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/number.

Unlike int, this field allows floating point numbers. It will give a validation error if the input is not a number, using the error value passed in through the invalid function.

import Form.Field as Field


example =
    Field.number
        { invalid =
            \value -> "Must be a number"
        }

Multiple Choice Fields

List ( String, option )
-> (String -> error)
-> Field
error
(Maybe option)
input
option
(Options option)
{ required : ()
, wasMapped : No
}

An input for a set of possible options. Can be rendered in two ways

import Form
import Form.Field as Field
import Form.FieldView as FieldView
import Form.Validation as Validation

sizeForm : Form.HtmlForm String Size input msg
sizeForm =
    (\size ->
        { combine =
            Validation.succeed identity
                |> Validation.andMap size
        , view =
            \formState ->
                [ Html.div []
                    [ FieldView.select []
                        (\entry -> ( [], sizeToString entry ))
                        size
                    ]
                , Html.button [] [ Html.text "Submit" ]
                ]
        }
    )
        |> Form.form
        |> Form.field "size"
            (Field.select
                [ ( "small", Small )
                , ( "medium", Medium )
                , ( "large", Large )
                ]
                (\_ -> "Invalid")
                |> Field.required "Required"
                |> Field.withInitialValue (\_ -> Small)
            )

sizeToString : Size -> String
sizeToString size =
    case size of
        Small ->
            "Small"

        Medium ->
            "Medium"

        Large ->
            "Large"

type Size
    = Small
    | Medium
    | Large
= AboveRange
| BelowRange

Used for errors from a range Field.

Date/Time Fields

{ invalid : String -> error }
-> Field
error
(Maybe Date)
input
Date
Input
{ min : Date
, max : Date
, required : ()
, wasMapped : No
, step : Int
}

A date field. Parses into a value of type Date.

example =
    Field.date
        { invalid = \_ -> "Invalid date" }
        |> Field.required "Required"
        |> Field.withMin (Date.fromRataDie 738624) "Must be after today"
        -- date picker will show dates on the same day of the week starting from the start date
        |> Field.withStep 7
{ invalid : String -> error }
-> Field
error
(Maybe TimeOfDay)
input
TimeOfDay
Input
{ min : TimeOfDay
, max : TimeOfDay
, required : ()
, wasMapped : No
, step : Int
}
type alias TimeOfDay =
{ hours : Int
, minutes : Int
, seconds : Maybe Int
}

A time of day in 24-hour time.

The hours must be between 0 and 23, and the minutes must be between 0 and 59.

This is the type that a time field parses into, and is also used to set initial values and minimum/maximum values for time.

See https://developer.mozilla.org/en-US/docs/Web/HTML/Date_and_time_formats#time_strings

Initial Values

(input -> initial)
-> Field error value input initial kind constraints
-> Field error value input initial kind constraints

Set an initial value for the Field given the Form's input (see Form.withInput). This allows you to pass in dynamic state like values from your Model.

The initial value will be used until the field is modified by the user, and from there it is controlled by user input. If you need to programmatically set a field's value for more advanced use cases, you can also modify the Form.Model.

The type you use to set the initial value depends on the Field. For example, you can set a checkbox Field's initial value with a Bool

example =
    Form.checkbox |> Form.withInitialValue .autoplay


formOptions : { autoplay : Bool } -> Form.Options String parsed { autoplay : Bool } msg
formOptions currentSettings =
    Form.options "settings"
        |> Form.withInput currentSettings

Note that the type used to set the initial value is independent of types you might map a field into.

(input -> Maybe initial)
-> Field error value input initial kind constraints
-> Field error value input initial kind constraints

Similar to withInitialValue, but takes in a Maybe value. If the Maybe is Nothing then it's the same as if no initial value were set.

example =
    Form.text |> Form.withOptionalInitialValue .nickname


formOptions : { nickname : Maybe String } -> Form.Options String parsed { nickname : Maybe String } msg
formOptions currentProfile =
    Form.options "profile"
        |> Form.withInput currentProfile

Other

String
-> error
-> Field
error
String
input
Never
Input
{ required : ()
, plainText : ()
, wasMapped : No
}

Render a field with a hardcoded value.

Field Configuration

error
-> Field
error
(Maybe parsed)
kind
input
initial
{ constraints | required : (), wasMapped : No }
-> Field
error
parsed
kind
input
initial
{ constraints | wasMapped : No }

Gives a validation error for fields that haven't been set, and removes the Maybe around the parsed value.

example =
    Field.int { invalid = \_ -> "Invalid" }
        -- parses into `Maybe Int` before we call `required`
        -- after `required`, it parses into an `Int`
        |> Field.required "Required"
(parsed -> Result error mapped)
-> Field error parsed input initial kind constraints
-> Field
error
mapped
input
initial
kind
{ constraints | wasMapped : Yes }

Add a custom validation and/or transformation of the value to the field.

import Form.Field as Field


example =
    Field.text
        |> Field.required "Required"
        |> Field.validateMap Username.fromString


  -- in Username.elm
  fromString : String -> Result String Username
  fromString string =
      if string |> String.contains "@" then
          Err "Must not contain @"


      else
          Username string |> Ok
map :
(parsed -> mapped)
-> Field error parsed input initial kind constraints
-> Field
error
mapped
input
initial
kind
{ constraints | wasMapped : Yes }

Map the parsed value of a Field without adding or modifying its validations or rendering.

import Form.Field as Field


example =
    Field.text
        |> Field.required "Required"
        |> Field.map String.toUpper

Text Field Display Options

Field
error
parsed
input
initial
Input
{ constraints | plainText : () }
-> Field error parsed input initial Input constraints

Modifier for text Field. This does not perform any additional validations on the Field, it only provides a hint to the browser that the Field should be displayed as an email input (<input type="email" />). This is especially useful for mobile devices to make sure the correct keyboard is displayed.

See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email.

example =
    Field.text
        |> Field.email
        |> Field.required "Email is required"
Field
error
parsed
input
initial
Input
{ constraints | plainText : () }
-> Field error parsed input initial Input constraints

Modifier for text Field. This is only a display hint to the browser that the Field should be displayed as a password input (<input type="password" />).

See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/password.

example =
    Field.text
        |> Field.password
        |> Field.required "Password is required"
Field
error
parsed
input
initial
Input
{ constraints | plainText : () }
-> Field error parsed input initial Input constraints

Modifier for text Field. This is only a display hint to the browser (<input type="tel" />).

This is especially important on mobile devices for ensuring that the correct keyboard is displayed for inputting a phone number.

See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/tel.

example =
    Field.text
        |> Field.telephone
url :
Field
error
parsed
input
initial
Input
{ constraints | plainText : () }
-> Field error parsed input initial Input constraints

Modifier for text Field. This does not perform any additional validations on the Field, it only provides a hint to the browser that the Field should be displayed as a URL input (<input type="url" />).

See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/url.

example =
    Field.text
        |> Field.url
        |> Field.required "URL is required"
{ rows : Maybe Int
, cols : Maybe Int
}
-> Field
error
parsed
input
initial
Input
{ constraints | plainText : () }
-> Field error parsed input initial Input constraints

Modifier for text Field to display it as a textarea.

textarea are for multi-line text input. For example, you might use a regular text Field for a username, and a textarea Field for a biography.

import Form.Field as Field


type alias Profile =
    { username : String
    , bio : String
    }


example =
    (\username bio ->
        { combine =
            Validation.succeed Profile
                |> Validation.andMap username
                |> Validation.andMap bio
        , view = []
        }
    )
        |> Form.form
        |> Form.field "username"
            (Field.text
                |> Field.required "Required"
            )
        |> Form.field "bio"
            (Field.text
                |> Field.textarea
                    { rows = Just 20
                    , cols = Just 50
                    }
                |> Field.required "Required"
            )

Numeric Field Options

{ min : numberInitial
, max : numberInitial
, missing : error
, invalid : OutsideRange -> error
}
-> Field
error
(Maybe valueType)
input
numberInitial
kind
{ constraints
| required : ()
, min : numberInitial
, max : numberInitial
, wasMapped : No
}
-> Field
error
valueType
input
numberInitial
Input
{ constraints | wasMapped : No }

Display a range input (<input type="range" />). See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/range.

import Form.Field as Field


type alias Settings =
    { brightness : Int
    }


example =
    (\brightness ->
        { combine =
            Validation.succeed Settings
                |> Validation.andMap brightness
        , view = []
        }
    )
        |> Form.form
        |> Form.field "brightness"
            (Field.range
                { min = 0
                , max = 100
                , missing = "Required"
                , invalid =
                    \outsideRange ->
                        case outsideRange of
                            Field.AboveRange ->
                                "Must be below 100"


                            Field.BelowRange ->
                                "Must be above 0"
                }
                (Field.int { invalid = \_ -> "Invalid" })
            )

Can be used with either int or float.

initial
-> error
-> Field
error
parsed
input
initial
kind
{ constraints | min : initial }
-> Field error parsed input initial kind constraints

Set the min value for the Field. This results in both a validation (run on the server as well as the client) as well as a display hint to the browser (<input type="date" min="2023-04-14" />). The Browser will prevent the user from entering a value below the min value in some cases but not all.

See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/min.

If the value is invalid (below the minimum), the error will be whichever error is passed in as the second argument.

import Date exposing (Date)
import Form.Field as Field


example =
    Field.date
        { invalid = \_ -> "Must be valid date" }
        |> Field.required "Required"
        |> Field.withMin today ("Must be after " ++ Date.toIsoString today)


today : Date
today =
    Date.fromRataDie 738624
initial
-> error
-> Field
error
parsed
input
initial
kind
{ constraints | max : initial }
-> Field error parsed input initial kind constraints

Same as withMin but for a maximum value. See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/max.

import Date exposing (Date)
import Form.Field as Field


example =
    Field.date
        { invalid = \_ -> "Must be valid date" }
        |> Field.required "Required"
        |> Field.withMax today "Cannot schedule more than 7 days in advance"


inAWeek : Date
inAWeek =
    Date.fromRataDie (today + 7)


today : Int
today =
    738624
Int
-> error
-> Field
error
parsed
input
initial
kind
{ constraints | minlength : () }
-> Field error parsed input initial kind constraints
Int
-> error
-> Field
error
parsed
input
initial
kind
{ constraints | maxlength : () }
-> Field error parsed input initial kind constraints
Int
-> Field
error
value
input
initial
view
{ constraints | step : Int }
-> Field error value input initial view constraints

Sets the step attribute on the form field for Field's with Int steps. See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/step.

For int fields, the step will change the up/down buttons in the field's UI to increment/decrement by the given step.

Int step values have different meanings for different kinds of Fields.

  • date - number of days
  • time - time in seconds

Phantom Options

type No

Used in the constraints for a Field. These can't be built or used outside of the API, they are only used as guardrails

to ensure sure that Fields are configured correctly.

type Yes

Used in the constraints for a Field. These can't be built or used outside of the API, they are only used as guardrails to ensure sure that Fields are configured correctly.