Thursday, February 8, 2018

Validating a form and displaying errors (7)

The previous post ended with code that had an error path for invalid forms, but no easy way to enter invalid data to try it out.

That's because the implemented registration form is so "open" - it has three text fields into which you can put anything you want.

I'm going to make the form a little bit more aggressive: I'm going to require the first and last name fields be non-empty (better hope you have two names else you can't come to play); and I'm going to further restrict the date of birth field to contain only numbers and the symbols / and -.

Validation rules can be added in the definition of a DF.Form. The form for registrations is defined like this:

registrationDigestiveForm initial = do
  Registration
    <$> "firstname" .: DF.string (Just $ firstname initial)
    <*> "lastname" .: DF.string (Just $ lastname initial)
    <*> "dob" .: DF.string (Just $ dob initial)

... and it defines each of the fields as being a DF.string. But that can be replaced with something user defined, such as this:

nonEmptyString def =
    (DF.check "This field must not be empty" (/= ""))
  $ DF.string def

... which declares a non-empty string: it's going to behave like a DF.string which takes any string, but additionally it will check that submitted string is not "", the empty string, and if it is, the form will fail to validate, giving the error "This field must not be empty".

So we can redefine registrationDigestiveForm like this:

registrationDigestiveForm initial = do
  Registration
    <$> "firstname" .: nonEmptyString (Just $ firstname initial)
    <*> "lastname" .: nonEmptyString (Just $ lastname initial)
    <*> "dob" .: nonEmptyString (Just $ dob initial)

That's enough to catch empty fields - try this now by deleting a name. The form will be submitted to the server, but what comes back will be the form again, ready for you to correct mistakes. There won't be any clues, though, about why this form wasn't validated.

For that, digestive-functors-blaze provides another HTML-generating function: DB.errorList. It will emit the list of validation errors for a named field, if there are any. So for each field in htmlForRegistration, alongside the function that generates that input field, we can add the list of errors:

htmlForRegistration view =
  B.form
    ! BA.method "post"
    $ do
      B.p $ do  "First name: "
                DB.errorList "firstname" view
                DB.inputText "firstname" view
      B.p $ do  "Last name: "
                DB.errorList "lastname" view
                DB.inputText "lastname" view
      B.p $ do  "Date of Birth: "
                DB.errorList "dob" view
                DB.inputText "dob" view
      B.p $     DB.inputSubmit "Save"

Now let's do some stricter validation on the date-of-birth field. Here's a predicate on a string that determines if our input will be valid:

isDateLike :: String -> Bool
isDateLike s = foldr (&&) $ map isDateChar s
  where 
    isDateChar c = c `elem` ("0123456789-/" :: String)

(That type signature :: String is needed because of an ambiguity that arises from elem being pretty vague about what container type it will take, and string literals being pretty vague about what type they will actually produce. You can grumble about the foldable/traversable proposal/problem here if you like. Or blame the people who keep inventing new string libraries.)

... and now use that to define:

dateLikeString def =
    (DF.check "This field must look like a date" isDateLike)
  $ nonEmptyString def

... and use it to define the date field like this:

...
    <*> "dob" .: dateLikeString (Just $ dob initial)
...

So now date of birth has two ways of failing: it can be empty, or it can be text that doesn't look like a date. (The empty string passes the date-like test, by the way, so you'll only get one error message if you leave it empty.)

The code changes for this post are in commit 07f46e83.

No comments:

Post a Comment