The FormBean library is a great way to render HTML forms, but unless you're an dab hand with CSS, plain forms can look rather dull.

With lots of web apps turning to the popular Bootstrap v3.3 CSS framework to brighten up their designs, this article looks at how to create a Bootstrap skin for your FormBeans...

The new Fantom Pod Repository powered by Eggbox uses a vanilla Bootstrap theme and uses the technique presented here to renders its forms...

This article will take a simple data class that records your favourite beer:

select all
class BeerDetails {
    @HtmlInput { type="text"; required=true}
    Str name

    @HtmlInput { type="select"; required=true}
    Beer beer

    @HtmlInput { type="checkbox"}
    Bool iAmRobot

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

enum class Beer {
    bitter, stout, lager, gingerBeer
}

The basic rendering doesn't supply any CSS styling and the form is rendered thus:

Plain Form Bean

Taking the name field as an example, it is rendered with the following HTML:

<div class="formBean-row inputRow name">
    <label for="name">Name</label>
    <input type="text" id="name" name="name" required value="">
</div>

Which is fine, although standard Bootstrap fields require the following class names:

<div class="form-group">
    <label for="name">Name</label>
    <input type="text" class="form-control" id="name" name="name" required value="">
</div>

Skinning the Text Field

We could add form-control to the HtmlInput facet:

@HtmlInput { type="text"; css="form-control"; required=true}
Str name

But we still need to use form-group in the outer div. So lets kill two birds with one stone and create a Bootstrap FormBean skin by extending InputSkin:

select all
const class BootstrapTextSkin : InputSkin {
    override Str render(SkinCtx skinCtx) {
        html    := Str.defVal
        attrs   := skinCtx.renderAttributes(["class":"form-control"])
        html    += """<div class="form-group">"""
        html    += """  <label for="${skinCtx.name}">${skinCtx.label}</label>"""
        html    += """  <input ${attrs} type="${skinCtx.formField.type}" value="${skinCtx.value}">"""
        html    += """</div>"""
        return html + "\n"
    }
}

We use skinCtx.renderAttributes(...) because that also renders any validation attributes.

To use the new BootstrapTextSkin, override the default skin in AppModule:

@Contribute { serviceType=InputSkins# }
static Void contributeInputSkins(Configuration config) {
    config.overrideValue("text", BootstrapTextSkin())
}

Our text box now looks like:

Form with Bootstrap Text Field

Adding Hints

Bootstrap also lets you define hints for your fields, so lets add one to the name field:

@HtmlInput { type="text"; required=true; hint="It's on your birth certificate!" }
Str name

Now let's optionally render it in the skin:

select all
const mixin BootstrapSkin : InputSkin {
    Str renderFormGroup(SkinCtx skinCtx, |Str->Str| inputStr) {
        html    := Str.defVal
        hint    := skinCtx.formField.hint ?: skinCtx.msg("field.${skinCtx.name}.hint")
        attMap  := ["class":"form-control"]
        if (hint != null)
            attMap["aria-describedby"] = "${skinCtx.name}-helpBlock"
        attrs   := skinCtx.renderAttributes(attMap)

        html    += """<div class="form-group">"""
        html    += """<label for="${skinCtx.name}">${skinCtx.label}</label>"""
        html    += inputStr(attrs)

        if (hint != null)
            html += """<span id="${skinCtx.name}-helpBlock" class="help-block">${hint}</span>"""

        html    += """</div>"""

        return html + "\n"
    }
}

Bootstrap Form with Hints

Skinning Errors

The basic FormBean error rendering is just a plain list:

Plain Form with Errors

To skin this we extend ErrorSkin and add it as a service. The FormBean class then picks this service up should it exist.

We'll render our errors in an danger themed alert.

select all
const class BootstrapErrorSkin : ErrorSkin {
    override Str render(FormBean formBean) {
        if (!formBean.hasErrors) return Str.defVal

        banner := formBean.messages["errors.banner"]

        html := ""
        html += """<div class="alert alert-danger" role="alert">"""
        html += banner
        html += """<ul>"""
        formBean.errorMsgs.each {
            html += """<li>${it}</li>"""
        }
        formBean.formFields.vals.each {
            if (it.errMsg != null)
                html += """<li>${it.errMsg}</li>"""
        }
        html += """</ul>"""
        html += """</div>"""

        return html
    }
}

And the service definition in AppModule:

static Void defineServices(RegistryBuilder bob) {
    bob.addService(ErrorSkin#, BootstrapErrorSkin#)
}

To give our text box a danger hue when it's in error, the BootstrapTextSkin needs to optionaly add the has-error class to the form-group div.

select all
const class BootstrapTextSkin : BootstrapSkin {
    override Str render(SkinCtx skinCtx) {
        ...
        errCss  := skinCtx.fieldInvalid ? " has-error" : Str.defVal
        ...
        html    += """<div class="form-group${errCss}">"""
        ...
        return html
    }
}

Our errors now look like this:

Bootstrap Form with Errors

Putting It All Together

What's left is styling the select, checkbox and button inputs. The skin code for the select and checkbox are in the completed code, but I'll not bore you with the details here. They're done in a similar vein as the default text input.

The FormBean.renderSubmit() method can't be skinned, but it does so little it doesn't matter, we can render our own button:

<button class='btn btn-primary' type='submit'>Submit</button>

Stick it all together and we have:

Bootstrap Form

The complete example code below has been tested with:

To use, save the code as Example.fan and run it as a Fantom script:

C:\fan Example.fan

Then view your work at http:/localhost:8080/

Have fun!

select all
using afIoc
using afBedSheet
using afFormBean

// ---- The BedSheet Web Application ------------------------------------------

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

// @SubModule only needed because this example is run as a script
@SubModule { modules=[FormBeanModule#] }
const class AppModule {

    static Void defineServices(RegistryBuilder bob) {
        bob.addService(ErrorSkin#, BootstrapErrorSkin#)
    }

    @Contribute { serviceType=Routes# }
    static Void contributeRoutes(Configuration conf) {
        conf.add(Route(`/`, BeerPage#render, "GET"))
        conf.add(Route(`/`, BeerPage#onPost, "POST"))
    }

    @Contribute { serviceType=InputSkins# }
    static Void contributeInputSkins(Configuration config) {
        config.overrideValue("email",       BootstrapTextSkin())
        config.overrideValue("text",        BootstrapTextSkin())
        config.overrideValue("url",         BootstrapTextSkin())
        config.overrideValue("password",    BootstrapTextSkin())
        config.overrideValue("checkbox",    BootstrapCheckboxSkin())
        config.overrideValue("textarea",    BootstrapTextAreaSkin())
        config.set          ("static",      BootstrapStaticSkin())
        config.overrideValue("select",      config.build(BootstrapSelectSkin#))
    }
}

class BeerPage  {
    @Inject
    HttpRequest? httpRequest

        @Inject { type=BeerDetails# }
    FormBean? formBean

    Text render() {
        Text.fromHtml(
            "<!DOCTYPE html>
             <html>
             <head>
                 <title>FormBean Bootstrap Skin Demo</title>
                 <link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css' />
             </head>
             <body class='container'>
                 <h2>Beer</h2>
                 <form action='/' method='POST' novalidate=''>
                     ${ formBean.renderErrors()   }
                     ${ formBean.renderBean(null) }
                     <button class='btn btn-primary' type='submit'>Submit</button>
                 </form>
             </body>
             </html>")
    }

    Text onPost() {
        // 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
        beerDetails := (BeerDetails) formBean.createBean

        // display a simple message
        return Text.fromPlain("Thank you ${beerDetails.name}.")
    }
}



// ---- Entities --------------------------------------------------------------

class BeerDetails {
    @HtmlInput { type="text"; required=true; hint="It's on your birth certificate!" }
    Str name

    @HtmlInput { type="select"; required=true; hint="It'd better not be Kool Aid!" }
    Beer beer

    @HtmlInput { type="checkbox"; hint="Tick if you're a robot" }
    Bool iAmRobot

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

enum class Beer {
    bitter, stout, lager, gingerBeer
}



// ---- Bootstrap FormBean Skins ----------------------------------------------

const mixin BootstrapSkin : InputSkin {
    Str renderFormGroup(SkinCtx skinCtx, |Str->Str| inputStr) {
        html    := Str.defVal
        errCss  := skinCtx.fieldInvalid ? " has-error" : Str.defVal
        hint    := skinCtx.formField.hint ?: skinCtx.msg("field.${skinCtx.name}.hint")
        attMap  := ["class":"form-control"]
        if (hint != null)
            attMap["aria-describedby"] = "${skinCtx.name}-helpBlock"
        attrs   := skinCtx.renderAttributes(attMap)

        html    += """<div class="form-group${errCss}">"""
        html    += """<label for="${skinCtx.name}">${skinCtx.label}</label>"""
        html    += inputStr(attrs)

        if (hint != null)
            html += """<span id="${skinCtx.name}-helpBlock" class="help-block">${hint}</span>"""

        html    += """</div>"""

        return html + "\n"
    }
}

const class BootstrapTextSkin : BootstrapSkin {
    override Str render(SkinCtx skinCtx) {
        renderFormGroup(skinCtx) |attrs| {
            """<input ${attrs} type="${skinCtx.formField.type}" value="${skinCtx.value}">"""
        }
    }
}

const class BootstrapTextAreaSkin : BootstrapSkin {
    override Str render(SkinCtx skinCtx) {
        renderFormGroup(skinCtx) |attrs| {
            """<textarea ${attrs}>${skinCtx.value}</textarea>"""
        }
    }
}

const class BootstrapStaticSkin : BootstrapSkin {
    override Str render(SkinCtx skinCtx) {
        renderFormGroup(skinCtx) |attrs| {
            """<p ${attrs}>${skinCtx.value.toXml}</p>"""
        }
    }
}

const class BootstrapSelectSkin : BootstrapSkin {
    @Inject private const   ValueEncoders       valueEncoders
    @Inject private const   OptionsProviders    optionsProviders

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

    override Str render(SkinCtx skinCtx) {
        renderFormGroup(skinCtx) |attrs| {
            html    := "<select ${attrs}>"

            formField := skinCtx.formField
            optionsProvider := formField.optionsProvider ?: optionsProviders.find(skinCtx.field.type)

            showBlank := formField.showBlank ?: optionsProvider.showBlank(formField)
            if (showBlank) {
                blankLabel := formField.blankLabel ?: optionsProvider.blankLabel(formField)
                html += """<option value="">${blankLabel?.toXml}</option>"""
            }

            optionsProvider.options(formField, skinCtx.bean).each |value, label| {
                optLabel := skinCtx.msg("option.${label}.label") ?: label
                optValue := skinCtx.toClient(value)
                optSelec := (optValue.equalsIgnoreCase(skinCtx.value)) ? " selected" : Str.defVal
                html += """<option value="${optValue}"${optSelec}>${optLabel}</option>"""
            }

            html    += "</select>"
            return html
        }
    }
}

const class BootstrapCheckboxSkin : InputSkin {
    override Str render(SkinCtx skinCtx) {
        hint    := skinCtx.formField.hint ?: skinCtx.msg("field.${skinCtx.name}.hint")
        checked := (skinCtx.value == "true" || skinCtx.value == "on") ? " checked" : Str.defVal
        attMap  := hint == null ? Str:Str[:] : ["aria-describedby" : "${skinCtx.name}-helpBlock"]
        attrs   := skinCtx.renderAttributes(attMap)
        html    := Str.defVal
        html    += """<div class="checkbox">"""
        html    += """<label>"""
        html    += """<input type="checkbox" ${attrs}${checked}> ${skinCtx.label}"""
        html    += """</label>"""

        if (hint != null)
            html += """<span id="${skinCtx.name}-helpBlock" class="help-block">${hint}</span>"""

        html    += """</div>"""
        return html + "\n"
    }
}

const class BootstrapErrorSkin : ErrorSkin {
    override Str render(FormBean formBean) {
        if (!formBean.hasErrors) return Str.defVal

        banner := formBean.messages["errors.banner"]

        html := ""
        html += """<div class="alert alert-danger" role="alert">"""
        html += banner
        html += """<ul>"""
        formBean.errorMsgs.each {
            html += """<li>${it}</li>"""
        }
        formBean.formFields.vals.each {
            if (it.errMsg != null)
                html += """<li>${it.errMsg}</li>"""
        }
        html += """</ul>"""
        html += """</div>"""

        return html
    }
}

Edits

  • 31 Jul 2016 - Updated example scripts to use FormBean 1.1, BedSheet 1.5, and IoC 3.0.
  • 3 Jul 2015 - Original article.

Discuss