Parse, don't validate form data.
FormData holds the form state; a wrapper for Dict.AnyMaybe a (our actual value, e.g. Maybe User) and input field error messagesparseDontValidate : List ( k, String ) -> ( Maybe a, List ( Maybe k, err ) )Instead of using your data type, wrap it with FormData
type alias User =
{ name : String
-- ... more fields
}
type alias Model =
- { userForm : User
+ { userForm : FormData FormField User
}
init flags =
- ( { userForm = User "" -- ... more fields
+ ( { userForm = FormData.init fieldToString []
}
, Cmd.none
)for type safety, use a custom type to enumerate the form fields
type FormField = Name | Age | Location -- ... more fields
fieldToString : FormField -> String
fieldToString k =
case k of
Name ->
"name"
-- ... more fieldsnot recommended, but you can use String to manage form fields too
type alias FormField = String
fieldToString = identityFor each form field we wire up the standard events (OnInput, OnBlur, and OnCheck; not 1-msg-per-input anymore!) and show any error for that field alongside its input
div []
[ input
[ onInput (OnInput Name)
, onBlur (OnBlur (Just Name))
, value (FormData.value Name model.userForm)
, type_ "text"
, placeholder "Name"
]
[]
, case FormData.errorAt (Just Name) errors of
Just error ->
small [] [ text error ]
Nothing ->
text ""
]And our update function hand off form state update to FormData helper functions
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
-- FormData standard wiring
OnInput k string ->
( { model | userForm = FormData.onInput k string model.userForm }
, Cmd.none
)
OnBlur k ->
( { model | userForm = FormData.onVisited k model.userForm }
, Cmd.none
)
OnCheck k bool ->
( { model | userForm = FormData.onCheck k bool model.userForm }
, Cmd.none
)
-- Our own form submit handling
Save user ->
( { model | userForm = FormData.onSubmit True model.userForm }
, Process.sleep 3000 -- TODO: perform some real work
|> Task.perform (always Saved)
)
Saved ->
( { model | userForm = FormData.onSubmit False model.userForm }
, Cmd.none
)Before working with form submission, we try to obtain our custom type value (and a list of errors) from our FormData
value using a parseDontValidate function we supply
let
( dataUser, errors ) =
FormData.parse parseDontValidate model.userForm
|> Tuple.mapSecond (FormData.visitedErrors model.userForm)Do a case dataUser of to manage our form element and submit button attributes
( formAttr, submitButtonAttr, submitButtonLabel ) =
case dataUser of
FormData.Invalid ->
( disabled True, disabled True, "Submit" )
FormData.Valid user ->
( onSubmit (Save user), disabled False, "Submit" )
FormData.Submitting user ->
( disabled True, disabled True, "Submitting..." )
in
form [ formAttr ]
[ -- form fields elided (see above)
button [ submitButtonAttr ] [ text submitButtonLabel ]
]See the example code running at https://elm-formdata.netlify.app or https://ellie-app.com/dRsLRCGJLCBa1