Last time, we broke up formlets into primitive applicative functors, then composed these primitives to produce formlets.
So far, if someone entered an unexpected value in our little implementation of formlets (e.g. "abc" in an int Formlet
), we'd get a nasty exception and there wasn't much we could do about it because we couldn't trace the exception to the formlet that was producing it. What we want, instead of an exception, is something that accumulates errors without interrupting the computation flow. Applicative functors, being oblivious, are perfect for that. More concretely, we want to:
- Know if the formlet was able to collect the value or not, and
- if it couldn't, get the formlet rendered with the values the user entered and error messages, so we can re-display this second form, giving the user a chance to correct his mistakes.
The first item is a job for the option type. We'll return Some v
if the formlet was successful and None
if it wasn't.
For the second, we already have a XmlWriter applicative, so we could just reuse it to build this "error form".
Recall the type of formlets as we defined it last time:
type 'a Formlet = 'a Environ XmlWriter NameGen
Adding the two concerns we just mentioned, the signature becomes:
type 'a Formlet = 'a Error XmlWriter Environ XmlWriter NameGen
where Error
is the Maybe monad in applicative form (remember that every monad is also an applicative functor).
This is what I was talking about when I mentioned formlets being extensible: you can extend formlet features by composing additional applicatives.
By the way, here's the same formlet type expressed using C# bracket style (which F# also supports):
type Formlet<'a> = NameGen<XmlWriter<Environ<XmlWriter<Error<'a>>>>>
See now why I prefer ML syntax? Bracket style looks quite messy once you start nesting many types.
Anyway, it's not enough to compose these applicatives, we also need some way to define how to validate things and how to show error messages. Let's define a validator type:
/// Validator type. /// Fst determines if value is valid /// Snd builds an error message type 'a Validator = ('a -> bool) * ('a -> xml_item list -> xml_item list)
and a function that attaches a validator to a formlet:
satisfies : 'a Validator -> 'a Formlet -> 'a Formlet
(I'm not going to bore you with implementation details in this post)
So we can now write:
let isInt = Int32.TryParse >> fst let intErrorMsg a xml = xml @ [ Tag("span", ["class","error"], [Text(sprintf "'%s' is not a valid number" a)]) ] let inputInt = input |> satisfies (isInt, intErrorMsg) |> lift int
When rendered the first time, this formlet doesn't show anything different:
printfn "%s" (render inputInt) <input name="input_0" value=""/>
If we enter an invalid value, validation kicks in:
let env = EnvDict.fromValueSeq ["input_0","abc"] match run inputInt env with | _, Some v -> printfn "Formlet successful, value %d" v | errorForm, None -> printfn "Formlet unsuccessful, error form: \n%A" (XmlWriter.render errorForm)
This will print:
Formlet unsuccessful, error form: <div> <input name="input_0" value="abc" /> <span class="error">'abc' is not a valid number</span> </div>
We might want to wrap all that XML/HTML manipulation in order to make defining validators easier, for example:
/// <summary> /// Constructs a validator /// </summary> /// <param name="isValid">Determines if value is valid</param> /// <param name="errorMsg">Builds the error message</param> let err (isValid: 'a -> bool) (errorMsg: 'a -> string) : 'a Validator = let addError value xml = [ Tag("span", ["class","errorinput"], xml) Tag("span", ["class","error"], [Text(errorMsg value)]) ] isValid, addError
Now we can code inputInt
as:
let inputInt = input |> satisfies (err isInt (sprintf "'%s' is not a valid number")) |> lift int
We can chain as many validators as we want for any formlet. For example, here's a formlet that checks that the submitted value falls within a specified range:
let isInRange min max n = n >= min && n <= max let inputRange min max = inputInt |> satisfies (err (isInRange min max) (fun _ -> sprintf "Value must be between %d and %d" min max)) let input10to40 = inputRange 10 40
Note how I used inputInt
as the starting point for inputRange
. inputInt
is already validated for integers, so any non-integers values fed to inputRange
will fail with the same error message as before. inputRange adds further validation on top of inputInt
's validation. This is another example of the composability of formlets.
I put up a fully-fledged implementation of formlets, including validation, on github. There are some minor differences with what I've written so far about formlets. I'll blog about these differences soon.
As with all my previous articles on formlets, I have to give credit to the Links team; these blog posts and code are mostly just a rehash of their papers, which I can't recommend enough.
No comments:
Post a Comment