Validating input in Elm
In this post I’ll describe a simple method for validating input in Elm. I’m using it in a voter engagement app I am working on, and I’ll describe it in that context. The app’s model has a voter
field, and a voter is a record:
{- LISTING 1 -}type alias Voter =
{ firstName : String
, lastName : String
, email : String
, zipcode : String
, birthday : String
, campaign : String
, points : Int
, registered : Bool
}
A voter who signs up to use the app enters the first five fields of Voter
and optionally the sixth. The validateInput
method in module Validation
checks the first five fields as described in LISTING 3 below. The validateInput
is called like this:
{- LISTING 2 -}signUpVoter model =
let
errorList =
validateInput model
in
if errorList == [] then
( { model | state = VotersSubmitted, errors = [] },
Request.doRequest
<| RequestData.signupParameters model )
else
( { model | state = InvalidInput, errors = errorList },
Cmd.none )
The output of validateInput model
is a list of errors — just a list of strings. If the list is empty, we proceed with normal output. If it is nonempty, we set the model variable message
to this list. The app’s view function takes care of displaying the message. (For an explanation of Request.doRequest
, see this article.)
Defining the validateInput function
The validateInput
function is a pipeline of functions with signature like the one below.
validatePassword : Model -> List String -> List String
The function validatePassword
takes the model and a list of errors as input. It then examines the model. If it finds an error, it adds it to the head of the list of errors and returns the updated list. If it finds no error, it passes the list of error on unchanged.
The validateInput
is a pipeline of functions that examine the model and add an error to the error list if necessary. The return value is therefore a list of all the errors discovered.
Take a look at LISTING 3 to see how the validation functions are defined. This “pipeline” approach is not rocket science, but it is a solution that is easy to maintain. To add another validation, define it so that it has the signature
Model -> List String -> List String
and add it to the pipeline. That’s all there is to it!
Here is the complete listing for the error-checking module.
{- LISTING 3 -}module Validation exposing (validateInput)import Types exposing (Model, Voter)validateInput : Model -> List String
validateInput model =
validateFirstName model.voter []
|> validateLastName model.voter
|> validateEmail model.voter
|> validatezipcode model.voter
|> validateBirthday model.voter
|> validatePassword modelvalidatePassword : Model -> List String -> List String
validatePassword model errorList =
if model.password == model.passwordAgain then
errorList
else
"Passwords do not match." :: errorListvalidateBirthday : Voter -> List String -> List String
validateBirthday voter errorList =
if (voter.birthday |> String.split ("/") |> List.length) == 3 then
errorList
else
"Date does not have three parts, as it should: 10/5/1999" :: errorListvalidateFirstName : Voter -> List String -> List String
validateFirstName voter errorList =
if voter.firstName == "" then
"First name must be nonempty" :: errorList
else
errorListvalidateLastName : Voter -> List String -> List String
validateLastName voter errorList =
if voter.lastName == "" then
"Last name must be nonempty" :: errorList
else
errorListvalidateEmail : Voter -> List String -> List String
validateEmail voter errorList =
if voter.email == "" then
errorList ++ [ "Email address can't be empty" ]
else if (voter.email |> String.split ("@") |> List.length) /= 2 then
"Invalid email address" :: errorList
else
errorListvalidatezipcode : Voter -> List String -> List String
validatezipcode voter errorList =
if (voter.zipcode |> String.length) /= 5 then
"zipcode must have five digits" :: errorList
else
errorList
Note. I would like to thank @akoppela for suggesting a more elegant way to add an element to a list.