Tuesday, March 29, 2011

Integrating Formlets with Wing Beats and Figment

I've blogged before about formlets, a nice abstraction of HTML forms. I started with a basic implementation, then showed validation. Now I'll present a non-toy implementation of formlets I called FsFormlets (really original name, I know). By "non-toy" I mean this implementation is not a proof of concept or just for didactic purposes, but is meant to be eventually production-quality.

I don't like to explain things in a vacuum, so I'll use a typical registration form to show how it works:

form2

Even though FsFormlets can be used to generate HTML on its own, there's a much better tool for this in F#: Wing Beats. Wing Beats is an EDSL in F# to generate HTML, much like SharpDOM in C# or Blaze in Haskell, or Eliom's XHTML.M in OCaml (except that XHTML.M actually validates HTML statically). So I've added a module to integrate FsFormlets to Wing Beats. This integration is a separate assembly; FsFormlets is stand-alone (it only requires .NET 3.5 SP1 and the F# runtime). We'll use FsFormlets to express forms, and Wing Beats to express the rest of HTML. Also, we'll handle web requests with Figment, with the help of some more glue code to integrate it with FsFormlets and Wing Beats.

Layout

Let's start by defining a layout in Wing Beats:

let e = XhtmlElement()

let layout title body = 
    [ 
        e.DocTypeHTML5 
        e.Html [ 
            e.Head [ 
                e.Title [ &title ] 
                e.Style [ 
                    &".error {color:red;}" 
                    &"body {font-family:Verdana,Geneva,sans-serif; line-height: 160%;}" 
                ] 
            ] 
            e.Body [ 
                yield e.H1 [ &title ] 
                yield! body 
            ] 
        ] 
    ]

No formlets so far, this is all pure HTML, expressed as a regular function. Now we'll build the form bottom-up.

ReCaptcha

FsFormlets already includes a reCaptcha formlet (I'll show its innards in a future post). We just have to configure it with a pair of public and private key (get it here) before using it:

let reCaptcha = reCaptcha {PublicKey = "your_public_key"; PrivateKey = "your_private_key"; MockedResult = None}

MockedResult lets you skip the actual validation web call and force a result, for testing purposes.

Date input

Now the date formlet, which is built from three inputs, plus labels and validation:

let f = e.Formlets

let dateFormlet : DateTime Formlet = 
    let baseFormlet = 
        yields t3 
        <*> (f.Text(maxlength = 2, attributes = ["type","number"; "min","1"; "max","12"; "required","required"; "size","3"]) |> f.WithLabel "Month: ") 
        <*> (f.Text(maxlength = 2, attributes = ["type","number"; "min","1"; "max","31"; "required","required"; "size","3"]) |> f.WithLabel "Day: ") 
        <*> (f.Text(maxlength = 4, attributes = ["type","number"; "min","1900"; "required","required"; "size","5"]) |> f.WithLabel "Year: ") 
    let isDate (month,day,year) = 
        let pad n (v: string) = v.PadLeft(n,'0') 
        let ymd = sprintf "%s%s%s" (pad 4 year) (pad 2 month) (pad 2 day) 
        DateTime.TryParseExact(ymd, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None) |> fst 
    let dateValidator = err isDate (fun _ -> "Invalid date") 
    baseFormlet 
    |> satisfies dateValidator 
    |> map (fun (month,day,year) -> DateTime(int year,int month,int day))

Here, baseFormlet is of type (string * string * string) Formlet, that is, it collects values in their raw form. This baseFormlet is then validated to make sure it's a date, and finally mapped to a DateTime. Note the use of the 'number' input type (a HTML5 input) and also HTML5 validation attributes (required, min, max)

Password input

The password formlet is next:

let doublePassword = 
    let compressedLength (s: string) = 
        use buffer = new MemoryStream() 
        use comp = new DeflateStream(buffer, CompressionMode.Compress) 
        use w = new StreamWriter(comp) 
        w.Write(s) 
        w.Flush() 
        buffer.Length 
    let isStrong s = compressedLength s >= 106L 
    let f = 
        yields t2 
        <*> (f.Password(required = true) |> f.WithLabel "Password: ") 
        <+ e.Br() 
        <*> (f.Password(required = true) |> f.WithLabel "Repeat password: ") 
    let areEqual (a,b) = a = b 
    f 
    |> satisfies (err areEqual (fun _ -> "Passwords don't match")) 
    |> map fst 
    |> satisfies (err isStrong (fun _ -> "Password too weak"))

There are two things we validate here for passwords: first, the two entered passwords must match; and second, it must be strong enough. To measure password strength I use the compression technique I described in my last post.

The <+ operator is used to add pure markup to the formlet, using Wing Beats.

Also note the 'required = true' parameter. In the date formlet, we just used required=required as an HTML attribute. Here we used the optional argument required=true, which validates the formlet both at the client (with a required=required HTML attribute) and the server (it has a satisfies clause). In the date formlet we don't really care to validate server-side if the user filled out each input, we just want to know if it's a valid date or not.

The optional parameter 'maxlength' does a similar thing: it outputs a maxlength attribute, and also checks the maximum length of he POSTed value. Sure all browsers implement maxlength properly, but anyone can easily craft a request (e.g. with cUrl, no need to program at all) to circumvent it. I see malicious bots doing things like this every day. This makes sure the data is really valid before you start processing it.

Putting it all together

Moving on, let's write the final formlet and store the posted information in a record:

type PersonalInfo = { 
    Name: string 
    Email: string 
    Password: string 
    DateOfBirth: DateTime 
} 

let registrationFormlet ip = 
    yields (fun n e p d -> 
                { Name = n; Email = e; Password = p; DateOfBirth = d }) 
    <*> (f.Text(required = true) |> f.WithLabel "Name: ") 
    <+ e.Br() 
    <*> (f.Email(required = true) |> f.WithLabel "Email: ") 
    <+ e.Br() 
    <*> doublePassword 
    <+ e.Br() 
    <+ &"Date of birth: " <*> dateFormlet 
    <+ e.Br() 
    <+ &"Please read very carefully these terms and conditions before registering for this online program, blah blah blah" 
    <+ e.Br() 
    <* (f.Checkbox(false) |> satisfies (err id (fun _ -> "Please accept the terms and conditions")) |> f.WithLabel "I agree to the terms and conditions above") 
    <* reCaptcha ip

Some notes about this last snippet:

  • f.Email(required = true) generates an <input type="email" required="required"/> (again, HTML5 !), all server-validated.
  • Unlike previous formlets, this one is a function, because reCaptcha needs the client's IP address to validate.

If you've read my previous posts about formlets, you may be wondering why I'm using things like f.Text() and f.Checkbox() (which are members of an object) instead of the regular functions input and checkbox. Those functions are also present in FsFormlets and you may use them interchangeably with the object-style formlets, e.g. instead of f.CheckBox(false) you can write checkbox false []. The object-style formlets build on top of functional formlets, adding optional parameters for validation. They also integrate more seamlessly with Wing Beats.

And we're done with the form! Now let's build the actual page that contains it. Using the layout we wrote earlier:

let s = e.Shortcut

let registrationPage form = 
    layout "Registration" [ 
        s.FormPost "" [ 
            e.Fieldset [ 
                yield e.Legend [ &"Please fill the fields below" ] 
                yield!!+form 
                yield e.Br() 
                yield s.Submit "Register!" 
            ] 
        ] 
    ]

This is also pure Wing Beats markup, I think it doesn't need much explanation except for the "yield!!+form" which indicates where to render the formlet (yes, the operator doesn't look very friendly, I'll probably change it). Note how easy it is to compose pieces of potentially reusable HTML with Wing Beats as they're just functions.

Handling requests with Figment

Now all we need to do is bind the formlet to a URL to render the page:

get "register" (fun _ -> registrationFormlet "" |> renderToXml |> registrationPage |> Result.wbview)

and handle the form POST:

post "register" (fun ctx -> 
    let env = EnvDict.fromFormAndFiles ctx.Request 
    match run (registrationFormlet ctx.IP) env with 
    | Success v -> Result.redirectf "thankyou?n=%s" v.Name 
    | Failure(errorForm,_) -> errorForm |> registrationPage |> Result.wbview)

Oh, I almost forgot the little "thank you" after-registration page:

get "thankyou" (fun ctx -> Result.contentf "Thank you for registering, %s" ctx.QueryString.["n"])

Now, this workflow we've just modeled is pretty common:

  1. Show form.
  2. User submits form.
  3. Validate form. If errors, show form again to user.
  4. Process form data.
  5. Redirect.

So it's worthy of abstraction. The only moving parts are the formlet, the page and what to do on successful validation, so instead of mapping get and post individually we can say:

formAction "register" { 
    Formlet = fun ctx -> registrationFormlet ctx.IP 
    Page = fun _ -> registrationPage 
    Success = fun _ v -> Result.redirectf "thankyou?n=%s" v.Name 
}

HTML5 ready

Implementation of HTML5 in browsers has exploded in 2010 (see the beautiful html5readiness.com for reference). In particular, HTML5 forms are already implemented in Chrome 10, Opera and Firefox 4 (with caveats). Safari and IE haven't implemented it yet (at least not in versions 5.0.4 and 9 respectively), but WebKit already supports it so I guess Safari and other WebKit-based browsers will implement this in the short-term. So there's little reason not to use the new elements and attributes right now, as long as you also have server-side validation.

For example, here's how Chrome validates required fields when trying to submit:

registration1

Whereas in Safari 5.0.4/Windows the server-side validation kicks in:

form-safari

If you want browser-side validation in Safari, IE, etc, you can easily use an unobtrusive polyfill. A thorough list of cross-browser HTML5 polyfills is available here.

For example, applying jQuery.tools is as easy as:

let jsValidation = 
    e.Div [ 
        s.JavascriptFile "http://cdn.jquerytools.org/1.2.5/full/jquery.tools.min.js" 
        e.Script [ &"$('form').validator();" ] 
    ]

and putting it at the bottom of the Wing Beats layout.

All code posted here is part of the sample Figment app.

FsFormlets source code is here.

No comments:

Post a Comment