Grails dialog plugin

Reference guide

What is it?

The jquery-dialog plugin provides an easy to setup and maintain framework for CRUD operations with Grails.
We found the scaffolding in Grails great, but the generated code is hard to maintain. By moving all the repetitive coding away from controllers and views, we brought the code that needs to be maintained in controllers and views to an absolute minimum. At the same time, we leveraged jQuery UI, jQuery datatables, Twitter Bootstrap, CKEditor and CodeMirror to provide a better user experience with dialog boxes, ajax-driven lists, master-detail views, tabs, hover help etc.

Features

  • Dialogs for creating and editing domain objects, with support for basic datatypes, 1:n and n:m relations and file attachments.
  • Dialogs for other purposes using command objects
  • List view of domain objects, a query, or 'something' provided by an api

The jquery-dialog plugin consists of:
  • Dialog services and list service
  • A generic list gsp
  • A taglib for dialogs
  • A helper Javascript library

We'll demonstrate the basic use of the jquery-dialog plugin by creating a small todo application.

The dialog plugin provides scripts that help setting up the application, the domain objects, controllers and views.

First, let's create the app:

grails create-app todo
cd todo

Since the plugin is not in the Grails plugin repository (yet) we have to install it the old-fashioned way.

Download the plugin and install it using the grails commandline:
 
grails install-plugin [path to] grails-jquery-dialog-[version].zip

Run the setup script:
grails setup-dialog-app

The setup-dialog-app sets up our resources.groovy, ApplicationResources.groovy, index.gsp and main.gsp. It's just copying in some template content.

Then, create a domain class for the todo items:

grails create-dialog-domain-class org.open_t.todo.TodoItem

The generated ToDoItem domain class is a normal domain class that has a static listConfig that defines the list view.

Let's add a description and a duedate to TodoItem. It now looks like this:

package org.open_t.todo
import org.open_t.dialog.*
class TodoItem {
    String description
    Date dueDate

    static constraints = {
    }
    // Configuration for list
    static listConfig=new ListConfig(name:'todoItem',controller: 'todoItem',newButton:true).configure {
        column name:'description',sortable:true
        column name:'dueDate',sortable:true
    }
}


Now we can generate the controller (in fact, we could have done this earlier, there is no dependency on the domain class other than the naming convention.

grails generate-dialog-controller org.open_t.todo.TodoItem

This will generate a controller capable of CRUD operations on our TodoItem class:

package org.open_t.todo
import grails.converters.JSON;

class TodoItemController {

    static allowedMethods = [submitdialog: "POST", delete: "POST"]

    def listService
    def dialogService
        
    def index = { redirect(action: "list", params: params) }

    def list = {
        render (view:'/dialog/list', model:[dc:TodoItem,listConfig:TodoItem.listConfig,request:request])
    }
    
    def jsonlist = {
        render listService.jsonlist(TodoItem,params,request) as JSON
    }
    
    def position() {
        render listService.position(TodoItem,params) as JSON
    }
    
    def dialog = { return dialogService.edit(TodoItem,params) }
    
    def submitdialog = { render dialogService.submit(TodoItem,params) as JSON }
                    
    def delete = { render dialogService.delete(TodoItem,params) as JSON }
    
}

Then generate the view:

grails generate-dialog-view org.open_t.todo.TodoItem

This will generate grails-app/views/todoItem/dialog.gsp:

<dialog:form object="${todoItemInstance}" >
    <dialog:table>
        <dialog:checkBox object="${todoItemInstance}" propertyName="completed" mode="edit" />
        <dialog:textField object="${todoItemInstance}" propertyName="description" mode="edit" />
        <dialog:date object="${todoItemInstance}" propertyName="dueDate" mode="edit" />
    </dialog:table>
</dialog:form>

Finally, generate a menu:

grails generate-dialog-menu

And i18n messages:

grails generate-dialog-messages

The generate-dialog-messages script only generates missing messages. So it is safe to re-run.

You should be able to run the app now:

grails run-app

Config.groovy

The only thing needed in Config.groovy is the location of the files and the corresponding external url:
dialog {
    files {
        baseUrl="http://localhost/files"
        basePath="/var/www/files"
    }
}

See the Files section for more details.

Resources

You need to set up the resources that will be used, and require them in a template in your application. The setup-dialog-app script sets this up for you:
 
modules = {    
    todo {
        dependsOn 'dialog,dialog-altselect,dialog-dataTables,bootstrap-responsive-css,bootstrap-tooltip,bootstrap-popover,bootstrap-modal,dialog-bootstrap,dialog-autocomplete,dialog-last'
    }
}
The following resource modules are available:
 
Module Description
dialog basic dialog functionality
dialog-altselect Multiple select widget
dialog-bootbox Bootbox dialog boxes (bootboxjs.com
dialog-bootstrap Twitter Bootstrap (twitter.github.io/bootstrap/)
dialog-ckeditor CKEditor HTML editor (ckeditor.com)
dialog-codemirror CodeMirror Syntax highlighting code editor (codemirror.net)
dialog-dataTables JQuery DataTables (www.datatables.net)
dialog-fileuploader Fileuploader (github.com/valums/file-uploader)
dialog-last Last (iniatlization of full-page dialog logic)
dialog-tree JSTree (jstree.com)
dialog-flot Flot charts (www.flotcharts.org)
dialog-autocomplete JQuery UI autocomplete (jqueryui.com/autocomplete)

Spring resources

We are using our own property editor for Date and BigDecimal types, this needs to be registered. The setup-dialog-app script sets this up for you:
import org.open_t.dialog.*;
beans = {
    dialogPropertyEditorRegistrar(DialogPropertyEditorRegistrar.class) {}    
}

Creating a dialog

A dialog follows the MVC model, as Grails naturally does. Most of the functionality that is needed in the controller is provided in the DialogService. By default, the action that is used for editing and creating domain objects is called 'dialog' and the associated view is 'dialog.gsp'.
The bit of controller code for create and update actions is simply:

def dialog = { return dialogService.edit(TodoItem,params) }
def submitdialog = { render dialogService.submit(TodoItem,params) as JSON }

The 'dialog' action functions as both a create and update action. If the id parameter is present, an update action is performed (so the associated domain object is fetched by the edit method), if not it will be a create action and a domain object will be constructed (with defaults).

The structure for a table-based form is as follows:

<dialog:form object="${...Instance}" >
    <dialog:table>
       ...        
    </dialog:table>
</dialog:form>
 

This form will submit to the action 'submitdialog' in the same controller.

Again, based on the presence of the id parameter either a create or update operation is performed.

For more information on the taglib, see the Dialog taglib reference guide

By default, the form is submitted to an action named as the dialog action prefixed with 'submit. So for the default dialog action that is 'submitdialog'. In simple cases all that's needed is a call to the dialogservice which will perform the create or update operation and return a ready-to-be-rendered-as-JSON object.

Adding help to form items

You can add help to form items by adding a .help entry in the messages file, like this:

todoItem.description.help=Please enter the description of this to do item

You can auto-generate help items in the messages file using the generate-dialog-messages script with the --with-help option. Only missing messages will be added:

grails generate-dialog-messages --with-help

Creating a tabbed dialog

A tabbed dialog works exactly as any normal dialog, only the view is different. A view for a tabbed dialog looks like this:
<dialog:form object="${...Instance}" >
    <dialog:tabs names="Name1,Name2" object="${...Instance}">
        <dialog:tab name="Name1" object="${...Instance}">
            ...
        </dialog:tab>
        <dialog:tab name="Name1" object="${...Instance}">
            ...
        </dialog:tab>
    </dialog:tabs>
</dialog:form>
 

Creating a page dialog

A page dialog works exactly as any normal dialog, only the view is not shown in a popup but as a full-page html page. A view for a page dialog looks for example like this:
<g:applyLayout name="main">
    <body name="...">
        <dialog:pageform name="..." error="${error}" class="span8 offset2 pageform-dialog">          
            <dialog:table>
                ...
            </dialog:table>
            <dialog:navigation buttons="backward,save" />
        </dialog:pageform>
    </body>
</g:applyLayout>
Since the dialog is shown directly as opposed to by the JQUery dialog code, the buttons at the bottom of the form are now part of the form. The <dialog:navigation> tag takes care of this. You can use this to create wizard-style dialogs, too.


Creating a menu

A simple drop-down menu can be generated with the grails generate-dialog-menu script:

grails generate-dialog-menu
The generated menu is in views/layouts/_menu.gsp and looks like this (this is from the todo list example):
<div class="nav-collapse collapse">
    <ul class="nav">
        <li class="">
            <a href="/todo" class="brand" >ToDo</a>
        </li>
        <dialog:dropdown code="todo">        
            <dialog:menuitem controller="todoItem" action="list" icon="icon-star"/>
        </dialog:dropdown>
    </ul>
</div>
The script justs generates all menu items in a single dropdown as it cannot guess how you want the menu to be organized. Obviously this is only useful as a starting point an you'll have to re-arrange the menu yourself.

How lists work

We are using JQuery datatables with an AJAJ (JSON) datasource to display lists. The initial view is just an empty table with the appropriate columns. The data is fetched by an AJAJ call that is done by JQuery datatables. This yields optimal performance when the contents of the list change due to pagination, sorting and filtering. As a consequence each list needs 2 actions: one for the initial view and one for the AJAJ call to supply content.
The dialog plugin provides a generic list view and the listService provides the necessary data for the AJAJ call, all based on the listConfig object in the domain class. This brings back the effort needed for most cases to an absolute minimum, all that is needed is 2 actions in the controller:

def list = {
    render (view:'/dialog/list', model:[dc:TodoItem,listConfig:TodoItem.listConfig,request:request])
}
    
def jsonlist = {
    render listService.jsonlist(TodoItem,params,request) as JSON
}

and a list configuration in the domain class:

static listConfig=new ListConfig(name:'todoItem',controller: 'todoItem',newButton:true).configure {
    column name:'description',sortable:true
    column name:'dueDate',sortable:true
    column name:'completed',sortable:true
}

Adding a search/filter box to a list

JQuery datatables features a search box that can be used to filter the contents of the list. To enable this, set the bFilter property in the ListConfig to true and set sortable true to true for each column that you want to search in:
static listConfig=new ListConfig(name:'todoItem',controller: 'todoItem',newButton:true,bFilter:true).configure {
    column name:'description',sortable:true,filter:true
    column name:'dueDate',sortable:true
    column name:'completed',sortable:true,filter:true
}

Query-based lists

Lists can also be based on a query rather than showing the all the objects of a domain class:
def openOrdersjsonlist() {
    def query = "from ShopOrder o where o.completed=false"       
    render listService.jsonquery(ShopOrder, params, request, query) as JSON
}

Drag and drop row reordering

JQuery datatables has a rowreordeing plugin that allows the user to reorder the contents of the list by dragging and dropping rows.
There needs to be a 'position' property in the domain class, and it needs to be the first column in the list.
To enable rowreordering, you need to set the rowreordering property of the listconfig to true and provide a 'position' action in the controller.
The listService has a 'position' method for this:
def position() {
    render listService.position(TodoItem,params) as JSON
}

DialogService

The dialogservice provides the backend for CRUD operations, so most CRUD actions in the controllers become one-liners that can be extended when something more than the CRUD operation is needed.
The edit method allows will fetch the domain instance that is indicated by the domain class and parameters provided, and delivers the model needed for the dialog.
For example, for our todo list item, the edit action becomes:
def dialog = { return dialogService.edit(TodoItem,params) }
Of course you will need a sumit action to go with that. By default, the name is the name of the dialog action prefixed with 'submit',
So for our todo item dialog action, the submisson action becomes:
def submitdialog = { render dialogService.submit(TodoItem,params) as JSON }
The submit method of the dialogService performs the create or update action and returns a ready-to-be-rendered-as-JSON object.
Obviously you can modify the object before rendering it. You can also provide a domain class instance as the third parameter and/or a closure as a fourth parameter to the submit method. The closure gives you access to the domainClassInstance and res objects.

ListService

The ListService provides the following methods:

jsonlist(dc,params,request,filterColumnNames=null,actions=null)

dc
The domain class to be used
params
The parameters (from the controller)
filterColumnNames
Column names to be used when filtering.
actions
Closure to dynamically generate the items for the actions column

jsonquery(dc,params,request,query,countQuery=null,listProperties=null,filterColumnNames=null,actions=null,queryParams=[:])

dc
The domain class to be used
params
The parameters (from the controller)
request
Request object
query
HQL query
countQuery
HQL count query should deliver a single row with the number of result objects int the first column
listProperies
List of names of the properties to be returned
fileterColumnNames
List of names of the properties to be used when filtering
actions
Closure to dynamically generate the items for the actions column
queryParams
Parameters for the HQL query
Show a list based on the provided query.

position(dc,params)

dc
The domain class to be used
params
The parameters (from the controller)
This method will perform a change of position of 2 items in a list.
The dialog plugin provides logic for 'attaching' files to a domain object. Files are stored on the filesystem. The directory structure is automatically generated/maintained. A folder is created for each domain object that has files attached. Files are partitioned into categories. Each category has it's own top-level directory so you can have different access/permission schemes per category.
When you want to use files in conjunction with a domain class you can add a <dialog:upload> tag to a dialog, like this:
<dialog:simplerow name="upload">
                <dialog:upload identifier="${contentInstance.id}" action="${createLink(controller:'content',action:'uploadfile')}" controller="content" direct="true"/>
            </dialog:simplerow>
This will result in the file to be submitted to the uploadfile action in the controller, which can simply call the uploadFile method of the FileService:
def uploadfile () {
    def res=fileService.uploadFile(request,params,"images",Content)
    render res as JSON
}
You can use a filesTable tag to show a list of files:
<dialog:filesTable object="${contentInstance}" newButton="false"/>
which will cal the filelist action to get a list of files. You can provide this as follows:
def filelist() {
    render fileService.filelist(Content,params,"images") as JSON
}
And a delete action:
def deletefile(){
    render fileService.deleteFile(Content,params,"images") as JSON
}

Finally, if you want a list of images available in ckeditor, you need to provide a 'filemap' action:
def filemap() {
    def filemap = fileService.filemap(Content,params,"images")       
    render(view:'/dialog/filemap',model:[filemap:filemap,CKEditorFuncNum:params.CKEditorFuncNum])
}

Javascript modules

The following modules are available:
 
Module Description
dialog basic dialog functionality
dialog-altselect Multiple select widget
dialog-bootbox Bootbox dialog boxes (bootboxjs.com
dialog-bootstrap Twitter Bootstrap (twitter.github.io/bootstrap/)
dialog-ckeditor CKEditor HTML editor (ckeditor.com)
dialog-codemirror CodeMirror Syntax highlighting code editor (codemirror.net)
dialog-dataTables JQuery DataTables (www.datatables.net)
dialog-fileuploader Fileuploader (github.com/valums/file-uploader)
dialog-last Last (iniatlization of full-page dialog logic)
dialog-tree JSTree (jstree.com)
dialog-flot Flot charts (www.flotcharts.org)
dialog-autocomplete JQuery UI autocomplete (jqueryui.com/autocomplete)

Events

JQuery events are used to notify widgets that something has happened that they should respond to. Events are sent to elements that have the approriate CSS class set. From 2.1 onwards the event classes are separated out rather than using dialog-events all the time which was the case before. this prevents mistakes and stray events triggering inadvertently triggering the event handlers too often.
 
Event name Sent to elements with CSS class Parameters Description
dialog-close dialog-close-events event:event
ui:ui
this:this
Sent when dialog is closed
dialog-message dialog-message-events message:The message text Sends text message.
dialog-open dialog-open-events event:event
ui:ui
this:this
id:id
controllerName:controllerName
Sent when dialog is opened to allow cotnrols to initialize themselves
dialog-refresh .dialog-refresh-events dc:domainClass
id:id
jsonResponse:result
The widget should refresh itself.
dialog-submit dialog-submit-events event:e
ui:ui
this:this
id:id
controllerName:controllerName
Sent before dialog submission
dialog-reload dialog-reload-events   Sent when element needs to reload remote content.