elm-form
elm-form copied to clipboard
Standalone version of the elm-pages Form API.
dillonkearns/elm-form 
Live Ellie demo: https://ellie-app.com/mzjFg6BWmMta1
elm-form is built around the idea
of managing a single Form.Model value as an unparsed set of raw field values and FieldStatus (blurred, changed, etc.).
This Form.Model can even handle form state of more than one form on a page, or even across multiple pages.
The package manages all of the unparsed state for you with a single Msg, a single Model entry, and then
uses your Form definition to run its validations against the unparsed values (Model),
and to render the form fields along with any validation errors.
If you use elm-form with elm-pages, the wiring is built into the framework so you don't need to wire in update or Model
yourself, and the framework manages additional Form state for you such as in-flight form submissions. The ideas in this
package originally came from elm-pages, but they are useful in a standalone context as well so this was split into
a separate package.
Some of these underlying ideas were discussed in the Elm Radio episode Exploring a New Form API Design.
Core Values
- Progressive Enhancement - to make things more robust, and to leverage existing standards to get features for free instead of re-inventing them for every app. Forms are great for sending data to servers, lets use them! Let's go back to our Web fundamentals - it's worth reading the MDN docs on form submissions to understand the core building blocks.
- Accessibility - using standards to provide an experience that supports a broad range of users and use cases
Core Ideas
Form.Validationlets you build up validations and parse fields into a combined value in the same pass (if you wanted to, you could even parse into aJson.Encode.Valueor some payload to send to an APIonSubmit)Form.Fieldlets you declare the fields (in the applicative pipeline in the Form definition)- You can pass an input value when you render the form which can be used in rendering the view, and for getting initial values (
withInitialValue)
Opinions
- Forms are always rendered within a
<form>element for accessibility, and to enable progressive enhancement - Fields are always rendered within a form field element of some kind ( or
Wiring
Many Elm form examples and APIs use the pattern of handling each changed field within
the update function. For example, elm-spa-example uses this pattern in the Settings route (and throughout the app).
❗️🛑 NOTE: This code below is NOT the pattern this package is built on ❗️🛑
type alias Model =
{ username : String
, avatar : String
-- ... an entry for each form field here
-- ... any additional app state
}
update msg model =
EnteredUsername username ->
updateForm (\form -> { form | username = username }) model
EnteredAvatar avatar ->
updateForm (\form -> { form | avatar = avatar }) model
-- .. additional handling for the remaining form fields
viewForm form =
Html.form [ onSubmit (SubmittedForm form) ]
[ input
[ onInput EnteredAvatar
, value form.avatar
-- other attributes
]
[]
-- , ... input elements for other form fields
]
This package tries to reduce boilerplate and manage form validations in a more declarative style
by parsing/validating the form as a whole rather than parsing/validating
each individual field. Here is the same Settings route with elm-pages and elm-form for reference.
✅👇 NOTE: the code below is the wiring pattern we use in this package. ✅👇
Instead of wiring in different Msg's and Model fields for each individual form field, the wiring in this package is done once for all form state like this:
type Msg
= FormMsg (Form.Msg Msg)
| OnSubmit (Form.Validated String SignUpForm)
-- | ... Other Msg's for your app
type alias Model =
{ formModel : Form.Model
, submitting : Bool
-- , ... additional state for your app
}
init : Flags -> ( Model, Cmd Msg )
init flags =
( { formModel = Form.init
, submitting = False
}
, Cmd.none
)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
OnSubmit parsed ->
case parsed of
Form.Valid signUpData ->
( { model | submitting = True }
, sendSignUpData signUpData )
Form.Invalid _ _ ->
-- validation errors are displayed already so
-- we don't need to do anything else here
( model, Cmd.none )
FormMsg formMsg ->
let
( updatedFormModel, cmd ) =
Form.update formMsg model.formModel
in
( { model | formModel = updatedFormModel }, cmd )
formView : Model -> Html Msg
formView model =
signUpForm
|> Form.renderHtml
{ submitting = model.submitting
, state = model.formModel
, toMsg = FormMsg
}
(Form.options "form"
|> Form.withOnSubmit (\{parsed} -> OnSubmit parsed)
)
[]
-- this is our parsed/validated type, but it can be anything we want,
-- including Json.Encode.Value, etc.
type alias SignUpForm =
{ username : String, password : String }
signUpForm : Form.HtmlForm String SignUpForm input msg
signUpForm =
(\username password passwordConfirmation ->
{ combine =
Validation.succeed SignUpForm
|> Validation.andMap username
|> Validation.andMap
(Validation.map2
(\passwordValue passwordConfirmationValue ->
if passwordValue == passwordConfirmationValue then
Validation.succeed passwordValue
else
Validation.fail "Must match password" passwordConfirmation
)
password
passwordConfirmation
|> Validation.andThen identity
)
, view =
\formState ->
let
fieldView label field =
Html.div []
[ Html.label []
[ Html.text (label ++ " ")
, FieldView.input [] field
, errorsView formState field
]
]
in
[ fieldView "username" username
, fieldView "Password" password
, fieldView "Password Confirmation" passwordConfirmation
, if formState.submitting then
Html.button
[ Html.Attributes.disabled True ]
[ Html.text "Signing Up..." ]
else
Html.button [] [ Html.text "Sign Up" ]
]
}
)
|> Form.form
|> Form.field "username" (Field.text |> Field.required "Required")
|> Form.field "password" (Field.text |> Field.password |> Field.required "Required")
|> Form.field "password-confirmation" (Field.text |> Field.password |> Field.required "Required")
This package is designed to be hooked into frameworks, whether it's a published framework like elm-pages (which has a built-in integration), or your own internal framework. See the elm-pages docs for more details on how to render and submit your form using elm-pages.