How to Improve Tapestry Errors Reporting When Using Bootstrap

Twitter recently released the Bootstrap framework. As I’m currently working on a recent project, I decided to give it a try on the Tapestry webapp.

So far, it proved to be easy to integrate. I introduced a Border component like this one:

package com.kalixia.iot.console.components;

import org.apache.tapestry5.annotations.Parameter;

public class Border {
    @Parameter(required = true, defaultPrefix = "literal")
    private String title;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}
<!DOCTYPE html>
<html lang="en" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd">
<head>
    <meta charset="utf-8"/>
    <title>My Website :: ${title}</title>
    <meta name="description" content="My Website :: ${title}"/>
    <!-- Le HTML5 shim, for IE6-8 support of HTML elements -->
    <!--[if lt IE 9]>
    <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
    <![endif]-->
    <link rel="stylesheet" href="http://twitter.github.com/bootstrap/assets/css/bootstrap-1.1.1.min.css"/>
    <link rel="shortcut icon" href="images/favicon.ico">
    <link rel="apple-touch-icon" href="images/apple-touch-icon.png">
    <link rel="apple-touch-icon" sizes="72x72" href="images/apple-touch-icon-72x72.png">
    <link rel="apple-touch-icon" sizes="114x114" href="images/apple-touch-icon-114x114.png">
</head>

<body>
    <t:body/>
</body>

</html>

Unfortunately, error messages where not displayed in a nice way. The end result was an ugly error message. After going through Bootstrap demo page, I was able to find out the required markup to add when displaying an error. The next part was to find out how to tweak Tapestry in such a way that it would render the errors using Bootstrap.

I found out that we are supposed to override the default ValidationDecorator. The easy way to do this is within your Module class:

public static void contributeMarkupRenderer(OrderedConfiguration<MarkupRendererFilter> configuration,
                                            final Environment environment) {
    MarkupRendererFilter validationDecorator = new MarkupRendererFilter() {
        public void renderMarkup(MarkupWriter writer, MarkupRenderer renderer) {
            ValidationDecorator decorator = new BootstrapValidationDecorator(environment, writer);
            environment.push(ValidationDecorator.class, decorator);
            renderer.renderMarkup(writer);
            environment.pop(ValidationDecorator.class);
        }
    };
    configuration.override("DefaultValidationDecorator", validationDecorator);
}

You then need to add the following class which does all the required markup stuff:

/**
 * Override default Tapestry validation decorator, so that CSS class of the "row"
 * is added an "error" CSS class if the field is in error.
 *
 * @author Jerome Bernard
 */
public class BootstrapValidationDecorator extends BaseValidationDecorator {
    private final Environment environment;
    private final MarkupWriter markupWriter;

    /**
     * @param environment  used to locate objects and services during the render
     * @param markupWriter
     */
    public BootstrapValidationDecorator(Environment environment, MarkupWriter markupWriter) {
        this.environment = environment;
        this.markupWriter = markupWriter;
    }

    @Override
    public void insideField(Field field) {
        if (inError(field))
            addErrorClassToUpperDivElement();
    }

    @Override
    public void afterField(Field field) {
        if (inError(field)) {
            markupWriter.element("span", "class", "help-inline");
            markupWriter.cdata(getErrorMessage(field));
            markupWriter.end();
        }
    }

    private void addErrorClassToUpperDivElement() {
        Element element = markupWriter.getElement();
        do {
            element = element.getContainer();
        } while (!element.getAttribute("class").contains("clearfix"));
        element.addClassName("error");
    }

    private boolean inError(Field field) {
        ValidationTracker tracker = environment.peekRequired(ValidationTracker.class);
        return tracker.inError(field);
    }

    private String getErrorMessage(Field field) {
        ValidationTracker tracker = environment.peekRequired(ValidationTracker.class);
        return tracker.getError(field);
    }
}

Displaying an error for a form field is way nicer now: Screenshot from a sample form

comments powered by Disqus