dillonkearns / elm-form / Form.Field


type alias Field error parsed input initial kind constraints =
Internal.Field.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

text : Field error (Maybe String) input String Form.FieldView.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 Basics.Bool input Basics.Bool Form.FieldView.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 Basics.Int) input Basics.Int Form.FieldView.Input { min : Basics.Int, max : Basics.Int, required : (), wasMapped : No, step : Basics.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"
        }

float : { invalid : String -> error } -> Field error (Maybe Basics.Float) input Basics.Float Form.FieldView.Input { min : Basics.Float, max : Basics.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

select : List ( String, option ) -> (String -> error) -> Field error (Maybe option) input option (Internal.Input.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


type OutsideRange
    = AboveRange
    | BelowRange

Used for errors from a range Field.

Date/Time Fields

date : { invalid : String -> error } -> Field error (Maybe Date) input Date Form.FieldView.Input { min : Date, max : Date, required : (), wasMapped : No, step : Basics.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

time : { invalid : String -> error } -> Field error (Maybe TimeOfDay) input TimeOfDay Form.FieldView.Input { min : TimeOfDay, max : TimeOfDay, required : (), wasMapped : No, step : Basics.Int }

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


type alias TimeOfDay =
{ hours : Basics.Int
, minutes : Basics.Int
, seconds : Maybe Basics.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

withInitialValue : (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.

withOptionalInitialValue : (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

exactValue : String -> error -> Field error String input Basics.Never Form.FieldView.Input { required : (), plainText : (), wasMapped : No }

Render a field with a hardcoded value.

Field Configuration

required : 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"

validateMap : (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

email : Field error parsed input initial Form.FieldView.Input { constraints | plainText : () } -> Field error parsed input initial Form.FieldView.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"

password : Field error parsed input initial Form.FieldView.Input { constraints | plainText : () } -> Field error parsed input initial Form.FieldView.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"

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

Modifier for text Field. This changes the display of the Field to a password input (<input type="search">). On mobile devices, this will display a keyboard with a search button.

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

example =
    Field.text
        |> Field.search

telephone : Field error parsed input initial Form.FieldView.Input { constraints | plainText : () } -> Field error parsed input initial Form.FieldView.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 Form.FieldView.Input { constraints | plainText : () } -> Field error parsed input initial Form.FieldView.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"

textarea : { rows : Maybe Basics.Int, cols : Maybe Basics.Int } -> Field error parsed input initial Form.FieldView.Input { constraints | plainText : () } -> Field error parsed input initial Form.FieldView.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

range : { 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 Form.FieldView.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.

withMin : 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

withMax : 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

withMinLength : Basics.Int -> error -> Field error parsed input initial kind { constraints | minlength : () } -> Field error parsed input initial kind constraints

Set a minimum length for the string. See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/minlength.

withMaxLength : Basics.Int -> error -> Field error parsed input initial kind { constraints | maxlength : () } -> Field error parsed input initial kind constraints

Set a maximum length for the string. See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/maxlength.

withStep : Basics.Int -> Field error value input initial view { constraints | step : Basics.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.

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.