Build type-safe, declarative forms with validation and transformation support.
elm-form provides a simple, composable way to create HTML forms in Elm. It uses phantom types to ensure inputs are properly configured at compile time, and offers a clean API for validation and value transformation.
elm install cekrem/elm-form
module Main exposing (main)
import Browser
import Dict exposing (Dict)
import Form
import Html exposing (Html)
import Html.Attributes as Attr
type alias Model =
{ formValues : Dict String String
}
type Msg
= FormChanged (Dict String String)
| FormSubmitted
init : Model
init =
{ formValues = Dict.empty }
emailValidator : String -> Result (List (Html.Attribute msg)) ()
emailValidator value =
if String.contains "@" value && String.contains "." value then
Ok ()
else
Err
[ Attr.class "error"
, Attr.attribute "aria-invalid" "true"
]
update : Msg -> Model -> Model
update msg model =
case msg of
FormChanged newValues ->
{ model | formValues = newValues }
FormSubmitted ->
-- Handle form submission
model
view : Model -> Html Msg
view model =
Html.div []
[ Form.new [ Attr.class "contact-form" ]
[ Form.input "name" "Full Name"
|> Form.withRequired True
|> Form.withTransformer String.trim
, Form.input "email" "Email Address"
|> Form.withType "email"
|> Form.withRequired True
|> Form.withValidator emailValidator
, Form.input "message" "Message"
|> Form.withRequired True
|> Form.withTransformer String.trim
]
|> Form.build model.formValues FormChanged FormSubmitted
]
main : Program () Model Msg
main =
Browser.sandbox
{ init = init
, update = update
, view = view
}The Form module provides functions for creating and managing entire forms:
Form - Opaque type representing a formnew - Create a form with attributes and inputsinput - Create a form input with a key and labelwithRequired - Mark an input as requiredwithType - Set the input type (text, email, password, etc.)withTransformer - Add value transformationwithValidator - Add validation with error attributeswithAttributes - Add custom HTML attributesbuild - Render the form to HTMLThe Input module provides low-level input building blocks with phantom types:
Input - Type-safe input with phantom type for state trackingDumb - Phantom type for inputs without interactionWithInteraction - Phantom type for inputs ready to rendernew - Create a basic inputwithOnChange - Add change handler (converts Dumb to WithInteraction)withDisabled - Mark as disabled (converts Dumb to WithInteraction)build - Render to HTML (requires WithInteraction)import Dict exposing (Dict)
import Form
import Html exposing (Html)
import Html.Attributes as Attr
type Msg
= FormChanged (Dict String String)
| FormSubmitted
passwordValidator : String -> Result (List (Html.Attribute msg)) ()
passwordValidator value =
if String.length value >= 8 then
Ok ()
else
Err
[ Attr.class "error"
, Attr.title "Password must be at least 8 characters"
, Attr.attribute "aria-invalid" "true"
]
view : Dict String String -> Html Msg
view formValues =
Html.div []
[ Form.new [ Attr.class "login-form" ]
[ Form.input "username" "Username"
|> Form.withRequired True
|> Form.withTransformer String.trim
|> Form.withAttributes [ Attr.autocomplete False ]
, Form.input "password" "Password"
|> Form.withType "password"
|> Form.withRequired True
|> Form.withValidator passwordValidator
]
|> Form.build formValues FormChanged FormSubmitted
]import Dict exposing (Dict)
import Form
import Html exposing (Html)
import Html.Attributes as Attr
type Msg
= FormChanged (Dict String String)
| FormSubmitted
emailValidator : String -> Result (List (Html.Attribute msg)) ()
emailValidator value =
if String.contains "@" value && String.contains "." value then
Ok ()
else
Err
[ Attr.class "error"
, Attr.attribute "aria-invalid" "true"
]
usernameValidator : String -> Result (List (Html.Attribute msg)) ()
usernameValidator value =
let
length =
String.length value
isAlphanumeric =
String.all (\c -> Char.isAlphaNum c || c == '_') value
in
if length >= 3 && length <= 20 && isAlphanumeric then
Ok ()
else
Err
[ Attr.class "error"
, Attr.title "Username must be 3-20 characters, alphanumeric or underscore"
]
strongPasswordValidator : String -> Result (List (Html.Attribute msg)) ()
strongPasswordValidator value =
let
hasLength =
String.length value >= 8
hasUpper =
String.any Char.isUpper value
hasLower =
String.any Char.isLower value
hasDigit =
String.any Char.isDigit value
in
if hasLength && hasUpper && hasLower && hasDigit then
Ok ()
else
Err
[ Attr.class "error"
, Attr.title "Password must be 8+ chars with upper, lower, and digit"
, Attr.attribute "aria-invalid" "true"
]
view : Dict String String -> Html Msg
view formValues =
Html.div []
[ Form.new [ Attr.class "registration-form" ]
[ Form.input "email" "Email"
|> Form.withType "email"
|> Form.withRequired True
|> Form.withValidator emailValidator
, Form.input "username" "Username"
|> Form.withRequired True
|> Form.withTransformer (String.trim >> String.toLower)
|> Form.withValidator usernameValidator
, Form.input "password" "Password"
|> Form.withType "password"
|> Form.withRequired True
|> Form.withValidator strongPasswordValidator
, Form.input "confirm" "Confirm Password"
|> Form.withType "password"
|> Form.withRequired True
]
|> Form.build formValues FormChanged FormSubmitted
]For more control, you can use the Input module directly:
import Input
import Html exposing (Html, div)
import Html.Attributes as Attr
type Msg
= NameChanged String
| EmailChanged String
view : String -> String -> Html Msg
view name email =
div []
[ Input.new "Name"
|> Input.withRequired True
|> Input.withPlaceholder "Enter your name"
|> Input.withTransformer String.trim
|> Input.withAttributes [ Attr.class "form-control" ]
|> Input.withOnChange NameChanged
|> Input.build name
, Input.new "Email"
|> Input.withType "email"
|> Input.withRequired True
|> Input.withPlaceholder "[email protected]"
|> Input.withOnChange EmailChanged
|> Input.build email
]The Input module uses phantom types to prevent rendering inputs without interaction handlers. This ensures you can't accidentally create an input that doesn't respond to user interaction:
-- This won't compile:
Input.new "Name"
|> Input.build ""
-- This will compile:
Input.new "Name"
|> Input.withOnChange NameChanged
|> Input.build ""Instead of returning error messages, validators return HTML attributes. This design:
Forms use Dict String String for state management because:
Transformers are applied during the onChange event, not during rendering. This ensures:
Contributions are welcome! Please ensure your changes:
BSD-3-Clause