.connect{ follow ->

}

Grails Part 4: Enhanced UI Design w/ Twitter Bootstrap

Click here to view the full tutorial
Continuing series: Developing a Grails Web Application.
This application will use the Grails Framework to develop a web application using:
1. MongoDB
2. Spring Security
3. Apache CXF
4. Jetty Web Server
5. Maven build integration => Removed upon Request
Previous Post: Grails Part 3 - Building a CRUD Application
View Project on Github

These Instructions Will Walk Through:

  • Enhanced UI Design with tw bootstrap
  • Configure Bootstrap with lesscss-resources plugin
  • Adding a custom controller
  • Customizing Scaffold Templates
  • Improving Scaffold Output

When building a user interface, it is important to first choose a style library. For this example we will be using bootstrap css and js to quickly enhance the user interface of our application. I also would like to be able to customize the default theme, so we will be adding the lesscss-resources plugin as well.

1. Configure Twitter Bootstrap

Head over to the Twitter Bootstrap Github Page and click on download the latest release in the Quick start section of the doc's.
  • Extract the files
  • Copy the bootstrap .js files into your project at web-app/js/bootstrap
  • Copy the bootstrap .less files into your project at web-app/less/bootstrap
  • Copy the /img/~.png files over to your projects web-app/images directory

2. Configure Bootstrap with Less compiler

First, add the new dependencies to your POM.
    <dependency>
        <groupId>org.grails.plugins</groupId>
        <artifactId>lesscss-resources</artifactId>
        <version>1.3.0.3</version>
        <scope>runtime</scope>
        <type>zip</type>
    </dependency>

Or if not using maven add the following to the grails-app/conig/BuildConfig.groovy with the other plugins: runtime ':lesscss-resources:1.3.0.3'

In the web-app/less directory add a file custom-bootstrap.less. Place an import tag at the top of the page to import the bootstrap less file. Include an override of the iconSpritePath's to point to your images directory.
@import "bootstrap/bootstrap.less";

// Sprite icons path
// -------------------------
@iconSpritePath:          "../images/glyphicons-halflings.png";
@iconWhiteSpritePath:     "../images/glyphicons-halflings-white.png";
This file will be used to override bootstraps UI properties in the future. This way we can do some customization of the default UI theme.
Finally, open the grails Config class and add the following lines to the bottom of the page.
Note: After looking back, this code belongs in AplicationResources.groovy config file. Here is a link to the AplicationResources.groovy
grails.resources.modules = {

    bootstrap {
        resource url:'less/custom-bootstrap.less',attrs:[rel: "stylesheet/less", type:'css']
        dependsOn 'jquery'
    }

}
To use bootstrap in any gsp page, add the tag <r:require modules="bootstrap"/> in the head.

Earlier we imported the lesscss-resources plugin. This integrates with bootstrap and will compile the bootstrap.less files into css. Now, custom-bootstrap.less compiles on top of the provided bootstrap.less to allow overriding bootstrap variables used to generate stylings. The variables that can be overridden are found on the official github site.(https://github.com/twitter/bootstrap/blob/v2.0.2/less/variables.less)

3. Adding a custom controller

Now, we're going to modify our PersonController. Edit to look like so:
class PersonController {

    def scaffold = Person

    def index = {
        def people = Person.list([sort:"lastName", order:"asc"])

        return [people: people]
    }
}
Next we need to create our first Groovy Server Page. To do this we create a file at the location myapp/grails-app/views/person/index.gsp
Add the following code to the groovy server page.
<@ page contentType="text/html;charset=UTF-8" >
<html xmlns="http://www.w3.org/1999/html" xmlns="http://www.w3.org/1999/html">
    <head>
        <meta name="layout" content="main"/>
        <r:require modules="bootstrap"/>
    </head>
    <body>
        <section>
            <div class="container">
                <div class="row">
                    <header class="page-header">
                        <h3>Person <small class="lead">Address List</small></h3>
                    </header>
                    <div class="span3">
                        <g:link class="btn btn-block btn-link" action="create">
                            Create New Person
                        </g:link>
                        <div class="well">
                            <ul class="nav nav-list">
                                <li class="nav-header">People</li>
                                <li class="active">
                                    <a id="view-all" href="#">
                                        <i class="icon-chevron-right pull-right"></i>
                                        <b>View All</b>
                                    </a>
                                </li>
                            <g:each in="${ people }" var="person" status="i">
                                <li>
                                    <a href="#Person-${person.id}">
                                        <i class="icon-chevron-right pull-right"></i>
                                        ${ "${ person.firstName } ${ person.lastName }" }
                                    </a>
                                </li>
                            </g:each>
                            </ul>
                        </div>
                    </div>
                    <div class="span9">
                    <g:each in="${ people }" var="person" status="i">
                        <div id="Person-${ person.id }" class="well well-small">
                            <table class="table table-bordered table-striped">
                                <caption>
                                    ${ "${ person.firstName } ${ person.lastName }" }: List of known addresses
                                </caption>
                                <thead>
                                    <tr>
                                        <th>State</th>
                                        <th>City</th>
                                        <th>Street</th>
                                        <th>Zip Code</th>
                                        <th>Options</th>
                                    </tr>
                                </thead>
                                <tbody>
                                <g:each in="${ person.addresses }" var="address">
                                    <tr>
                                        <td>${ address.state }</td>
                                        <td>${ address.city }</td>
                                        <td>${ address.streetAddress }</td>
                                        <td>${ address.zipCode }</td>
                                        <td><g:link class="btn btn-small btn-inverse" controller="address"
                                                    action="edit" id="${address.id}">
                                                <i class="icon-edit icon-white"></i>
                                            </g:link>
                                        </td>
                                    </tr>
                                </g:each>
                                </tbody>
                            </table>
                            <div class="btn-group">
                                <g:link class="btn btn-primary" action="edit" id="${person.id}">
                                    <i class="icon-edit icon-white"></i>Edit
                                </g:link>
                            </div>
                        </div>
                    </g:each>
                    </div>
                </div>
            </div>
        </section>
        <g:javascript>
            $('ul.nav > li > a').click(function(e){
                if($(this).attr('id') == "view-all"){
                    $('div[id*="Person-"]').fadeIn('fast');
                }else{
                    var aRef = $(this);
                    var tablesToHide = $('div[id*="Person-"]:visible').length > 1
                            ? $('div[id*="Person-"]:visible') : $($('.nav > li[class="active"] > a').attr('href'));

                    tablesToHide.hide();
                    $(aRef.attr('href')).fadeIn('fast');
                }
                $('.nav > li[class="active"]').removeClass('active');
                $(this).parent().addClass('active');
            });
        </g:javascript>
    </body>
</html>

***Note: We don't need to define any url mappings for the index. By default groovy will recognize that PersonController.index() maps to views/person/index.gsp.

Next, open the view: views/layouts/main.gsp. Remove any reference links to css files, except the main.css. Ensure that the tag <r:layoutResources /> is declared at both the bottom of the head and body tags. It should look similar to this.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
        <title><g:layoutTitle default="Grails Mongo"/></title>
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="shortcut icon" href="${resource(dir: 'images', file: 'favicon.ico')}" type="image/x-icon">
        <link rel="apple-touch-icon" href="${resource(dir: 'images', file: 'apple-touch-icon.png')}">
        <link rel="apple-touch-icon" sizes="114x114" href="${resource(dir: 'images', file: 'apple-touch-icon-retina.png')}">
        <link rel="stylesheet" href="${resource(dir: 'css', file: 'main.css')}" type="text/css">
        <g:layoutHead/>
        <r:layoutResources />
    </head>
    <body>
        <g:layoutBody/>
        <div id="spinner" class="spinner" style="display:none;"><g:message code="spinner.alt" default="Loading&hellip;"/></div>
        <g:javascript library="application"/>
        <r:layoutResources />
    </body>
</html>


Also, edit the main.css file. Remove all css except for the section's commented /* PAGINATION */ and /* MESSAGES AND ERRORS */
Finally, go into the Configuration Class: URLMappings and modify as such:
class UrlMappings {

    static mappings = {
        "/$controller/$action?/$id?"{
            constraints {
                // apply constraints here
            }
        }

        "/"(controller: "person", action: "index")
        "500"(view:'/error')
    }
}
Note: The grails runtime supports hot swapping of gsp files except for the template files. It seems to work with URLMappings, but I haven't tested what classes are supported and aren't.

4. Customize Scaffold Templates

We are going to customize the default scaffolding template. This will allow us to keep using scaffolding for our basic CRUD operations.
In console run: grails install-template
Copies the the templates used by Grails during code generation to your project directory at src/templates
  1. Add the tag <r:require modules="bootstrap"/> below <meta name="layout" content="main"> in each of the views: show, list, create, edit
  2. Wrap each of the views with a <div class="container"></div> just inside the body
  3. Modify the nav classes. Wrap the Navigation list with a <div class="navbar"></div>
    • Change the div wrapping the list <div role="navigation"> to <div class="nav">
    • Add the class nav to the ul tag like so: <ul class="nav">
  4. Modify edit, show, create
    • Modify the tag: <fieldset class="buttons"> to <fieldset class="form-actions">
    • In show.gsp replace all class names "property-value" with "uneditable-input"
    • Change this span in show.gsp <span id="${p.name}-label"...> to a label tag.
  5. Modify list.gsp
    • The table class should look as follows: <table class="table table-bordered table-striped">
Feel free to play around with these views more. While, I'm leaving the instructions simple, it is likely you will want to clean up the template views a little more.

5. Improve Scaffold Output

One thing we would like to do is add proper error messages to CRUD forms. For each validation we can define an error message in the grails-app/i8n/messages.properties file. This file can map validation errors to their associated messages. Insert these lines at the bottom of this file.
person.firstName.blank=first name cannot be left blank
person.lastName.blank=last name cannot be left blank

address.streetAddress.blank=Street Address cannot be left blank
address.city.blank=City cannot be left blank
address.zipCode.blank=Zipcode cannot be left blank
address.zipCode.validator=Zipcode must be a valid number
address.zipCode.size=Zipcode must be a valid 5 digit number
address.state.blank=State cannot be left blank
address.state.size=State must be entered as a valid state code. Ex: NY
Next, we want to display the Person when displaying an Address, and display the Addresses when displaying a person. To do this, we override the toString method of each domain class.
So in Person
    String toString(){ return "${firstName} ${lastName}"}
And in Address
    String toString(){ return "${streetAddress} ${city}, ${state}, ${zipCode}"}
The application is now setup on bootstrap. Go ahead and run the application!

Click here to view the full tutorial