This article expands on the QuickStart example in the FormBean documentation by adding a javascript date picker to Date fields.

The problem

The FormBean library lets you edit Fantom objects by rendering them as HTML forms. It handles all the conversions to and from HTML and even performs basic client and server side validation.

The QuickStart example in the FormBean documentation shows how to edit text, email addresses, URLs and numbers. But it kinda skips over dates.

Dates are problematic.

When it comes to inputs and validation, FormBean takes the approach of letting HTML5 do all the heavy lifting. Unfortunately this doesn't work with dates because there is no consistent means to enter or edit them. At the time of writing Firefox renders date fields a plain text box with no validation, and even Chrome's implementation is poor and borders on unusable.

More that than, the HTML5 spec doesn't specify a way to format the date. An ugly ISO format is your only display option.

If you don't believe me, let's try it out. Let's take the example from FormBean's QuickStart example and add a Date of Birth field to the ContactDetails class:

@HtmlInput { type="date" }
Date dob

Running the example and viewing the page in Firefox v33 gives:

Default Date Input in Firefox

See, just a plain textbox! So, the only real option left open is to use a javascript framework. Luckily, this isn't difficult to integrate at all.

Bootstrap Datepicker

Bootstrap Datepicker The datepicker I prefer is the Bootstrap Datepicker by Eternicode. You can play with its options here in the sandbox

It may be called Bootstrap Datepicker but as we'll see, it doesn't have to be used with Bootstrap apps.

Looking at the Datepicker example and reading the docs it seems we need to do the following to convert an ordinary text field to a datepicker field:

  • load the jQuery javascript library
  • load the Datepicker javascript library
  • load the Datepicker CSS
  • initialise the text field with javascript

All these points are in the realm of the Duvet library so we'll use that. In fact, it is very similar to just using the Bootstrap framework so it's probably worthwhile you reading the Bootstrap and BedSheet article.

Duvet Configuration

To configure Duvet we'll add two javascript libraries; jquery and datepicker. We'll have datepicker depend on jquery so when datepicker is referenced, jquery would have already been loaded:

select all
@Contribute { serviceType=ScriptModules# }
static Void contributeScriptModules(Configuration config) {
    jQuery := ScriptModule("jquery")
        .atUrl(`//code.jquery.com/jquery-1.11.1.min.js`)

    picker := ScriptModule("datepicker")
        // Note you should host the datepicker javascript code yourself
        .atUrl(`http://eternicode.github.io/bootstrap-datepicker/bootstrap-datepicker/js/bootstrap-datepicker.js`)
        .requires("jquery")

    config.add(jQuery)
    config.add(picker)
}

DateSkin

To render the Date field as a datepicker we'll override the default date skin with our own. Our DateSkin class will be invoked every time a datepicker is rendered so this is a good time to inject the stylesheet and call our initialising script. It means we only inject stuff into the page when it's actually needed.

We don't have to worry if multiple datepickers are rendered on the same page, because Duvet will ensure the CSS and Javascript libraries only get loaded the once.

So first we'll override the default DateSkin with our own.

@Contribute { serviceType=InputSkins# }
static Void contributeInputSkins(Configuration config) {
    config.overrideValue("date", config.build(DateSkin#))
}

Note how we autobuild the DateSkin class. This is because (as you're about see) we want to @Inject the Duvet HtmlInjector into it.

select all
using afIoc
using afDuvet
using afFormBean

const class DateSkin : DefaultInputSkin {

    @Inject private const HtmlInjector injector

    new make(|This| in) { in(this) }

    override Str renderElement(SkinCtx skinCtx) {

        // Note you should host the datepicker CSS yourself
        injector.injectStylesheet.fromExternalUrl(`http://eternicode.github.io/bootstrap-datepicker/bootstrap-datepicker/css/datepicker3.css`)

        injector.injectRequireScript(
            ["datepicker":"datepicker", "jquery":"\$"],
            "\$('#${skinCtx.name}').datepicker({format:'d M yyyy', autoclose: true });"
        )

        return "<input class='date' type='text' ${skinCtx.renderAttributes} value='${skinCtx.value}'>"
    }
}

The 3 lines in our new DateSkin are pretty straight forward:

  • inject the CSS stylesheet,
  • inject the javascript initialiser code,
  • return the input HTML.

ValueEncoder

While the DateSkin takes care of the client side, we also need to take care of the server side. This boils down to being able to convert a Fantom Date object to and from a string. To do that we contribute a ValueEncoder. So in your AppModule:

@Contribute { serviceType=ValueEncoders# }
static Void contributeValueEncoders(Configuration config) {
    config[Date#] = config.build(DateValueEncoder#)
}

The actual DateValueEncoder is straight forward:

select all
using afBedSheet

const class DateValueEncoder : ValueEncoder {
    const Str datePattern := "D MMM YYYY"

    override Str toClient(Obj? value) {
        if (value == null) return ""
        return ((Date) value).toLocale(datePattern)
    }

    override Obj? toValue(Str clientValue) {
        if (clientValue.isEmpty) return null
        return Date.fromLocale(clientValue, datePattern)
    }
}

For displaying dates, the format I always use is:

23 Mar 1979

To me, it's a no-brainer. It's an English format (days first, yay!) and because the month is abbreviated, it can not be mistaken for any other date; unlike 03-04-1979 which means different things in the UK and the US.

To ensure the date format is the same in both the client side datepicker and the server side, we use d M yyyy in DateSkin and D MMM YYYY in DateValueEncoder.

Put It All Together

Bootstrap Datepicker

Now we can put his all together in the FormBean QuickStart example. Note that the following example has been written and tested with:

But is known to work with:

  • IoC 3.0.4
  • BedSheet 1.5.6
  • FormBean 1.1.8
  • Duvet 1.1.4
  • bootstrap-datepicker 1.6.4
  • jQuery 2.1.4
select all
using afIoc
using afBedSheet
using afFormBean
using afDuvet

class ContactUsPage  {
    @Inject
    HttpRequest httpRequest

    @Inject { type=ContactDetails# }
    FormBean formBean

    new make(|This|in) { in(this) }

    Text render() {
        Text.fromHtml(
            "<!DOCTYPE html>
             <html>
             <head>
                 <title>FormBean Demo</title>
                 <link rel='stylesheet' href='/styles.css'>
             </head>
             <body>
                 <h2>Contact Us</h2>
                 <span class='requiredNotification'>* Denotes Required Field</span>

                 <form class='contactForm' action='/contact' method='POST'>
                     ${ formBean.renderErrors()   }
                     ${ formBean.renderBean(null) }
                     ${ formBean.renderSubmit()   }
                 </form>
             </body>
             </html>")
    }

    Text onContact() {
        // perform server side validation
        // if invalid, re-render the page and show the errors
        if (!formBean.validateForm(httpRequest.body.form))
            return render

        // create an instance of our form object
        contactDetails := (ContactDetails) formBean.createBean

        echo("Contact made!")
        echo(" - name:    ${contactDetails.name}")
        echo(" - email:   ${contactDetails.email}")
        echo(" - website: ${contactDetails.website}")
        echo(" - dob:     ${contactDetails.dob}")
        echo(" - message: ${contactDetails.message}")

        // display a simple message
        return Text.fromPlain("Thank you ${contactDetails.name}, we'll be in touch.")
    }
}

class ContactDetails {
    @HtmlInput { required=true; attributes="placeholder='Fred Bloggs'" }
    Str name

    @HtmlInput { type="email"; required=true; placeholder="fred.bloggs@example.com"; hint="Proper format 'name@something.com'" }
    Uri email

    @HtmlInput { type="url"; required=true; placeholder="http://www.example.com"; hint="Proper format 'http://someaddress.com'" }
    Str website

    @HtmlInput { type="date" }
    Date dob

    @HtmlInput { type="textarea"; required=true; attributes="rows='6'"}
    Str message

    new make(|This|in) { in(this) }
}

// @SubModule only needed because this example is run as a script
@SubModule { modules=[FormBeanModule#, DuvetModule#] }
const class AppModule {
    @Contribute { serviceType=Routes# }
    static Void contributeRoutes(Configuration conf) {
        conf.add(Route(`/`, ContactUsPage#render))
        conf.add(Route(`/contact`, ContactUsPage#onContact, "POST"))

        // to save you typing in a stylesheet, we'll just redirect to one I made earlier
        // conf.add(Route(`/styles.css`, `styles.css`.toFile))
        conf.add(Route(`/styles.css`, Redirect.movedTemporarily(`http://static.alienfactory.co.uk/fantom-docs/afFormBean-quickStart.css`)))
    }

    @Contribute { serviceType=ScriptModules# }
    static Void contributeScriptModules(Configuration config) {
        jQuery := ScriptModule("jquery")
            .atUrl(`//code.jquery.com/jquery-1.11.1.min.js`)

        picker := ScriptModule("datepicker")
            .atUrl(`https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.6.1/js/bootstrap-datepicker.min.js`)
            .requires("jquery")

        config.add(jQuery)
        config.add(picker)
    }

    @Contribute { serviceType=InputSkins# }
    static Void contributeInputSkins(Configuration config) {
        config.overrideValue("date", config.build(DateSkin#))
    }

    @Contribute { serviceType=ValueEncoders# }
    static Void contributeValueEncoders(Configuration config) {
        config[Date#] = config.build(DateValueEncoder#)
    }
}

const class DateSkin : DefaultInputSkin {
    @Inject private const HtmlInjector injector

    new make(|This| in) { in(this) }

    override Str renderElement(SkinCtx skinCtx) {
        // Datepicker needs some default Bootstrap styles
        injector.injectStylesheet.fromExternalUrl(`https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.3.2/css/bootstrap.min.css`)
        // Note you should host the datepicker CSS yourself
        injector.injectStylesheet.fromExternalUrl(`https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.6.1/css/bootstrap-datepicker.min.css`)

        injector.injectRequireScript(
            ["datepicker":"datepicker", "jquery":"\$"],
            "\$('#${skinCtx.name}').datepicker({format:'d M yyyy', autoclose: true });"
        )

        return "<input class='date form-control' type='text' ${skinCtx.renderAttributes} value='${skinCtx.value}'>"
    }
}

const class DateValueEncoder : ValueEncoder {
    const Str datePattern := "D MMM YYYY"

    override Str toClient(Obj? value) {
        if (value == null) return Str.defVal
        return ((Date) value).toLocale(datePattern)
    }

    override Obj? toValue(Str clientValue) {
        if (clientValue.isEmpty) return null
        return Date.fromLocale(clientValue, datePattern)
    }
}

class Main {
    static Void main() {
        BedSheetBuilder(AppModule#).startWisp(8069)
    }
}

Have fun!

Edits

  • 20 May 2016 - Updated example to work with Formbean 1.1, Ioc 3, BedSheet 1.5, and Duvet 1.1.
  • 7 December 2014 - Original article.

Discuss