Hey everybody,
I’m excited to share a new package for parsing and rendering forms in Elm! You can check out the package docs here: dillonkearns/elm-form
.
Here’s an Ellie demo that shows two forms rendered on the same page in a vanilla Elm app: https://ellie-app.com/myVVqSVC2QZa1
The Big Idea
The thesis behind this package is that a Form library can take on more responsibility for Form state and wiring if we have a cleaner separation between representing form data (unstructured String key-value pairs) and parsing form data (this part needs to know about the user’s code because it parses into the user’s preferred data types).
That means we can reduce the boilerplate down to this:
type Msg
= FormMsg (Form.Msg Msg)
-- | ... 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 if we don't want to
( 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 "signUpForm"
|> Form.withOnSubmit OnSubmit
)
[]
The Form.Model
type stores a Dict
of state, so we don’t need to change our Model
or Msg
to manage additional form state (you can see an example of that in the Ellie demo).
Defining the Form
Here’s an example of a Form
definition. Note that we get client-side validations from these definitions, including a check that depends on other form fields in the case of the password confirmation field in this example.
type alias SignUpForm =
{ username : String, password : String }
signUpForm : Form.HtmlForm String SignUpForm input
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")
Serializing Data
You can take this idea a step further if you use code sharing to re-use your Form
on an Elm backend. This package was originally the Form API in the elm-pages
v3 beta, but I got some requests to make it available as a standalone package. But if you have the ability to do full-stack Elm, as with the elm-pages v3 beta or with Lamdera, you can even use a single OnSubmit
Msg to send the raw form data (List ( String, String)
), then parse that on the backend using the same Form
definitions.
Delegating to Your Framework
You can reduce the boilerplate even further if you move some of the responsibility into your framework (whether it’s a published tool or an internal framework). elm-pages
manages form submissions automatically using the idea of progressive enhancement to emulate the built-in Browser behavior when you submit a form. This means that the framework keeps track of which form is submitting so you don’t need to wire that up every time you create a new form. It even uses a single application-level Msg to let the framework perform form submissions through its top-level update
function so you don’t need to define that logic for every form or every page in your app.
I’d love to hear what people build with this new package. Feedback is very welcome. Hope it’s useful!