Thursday, November 27, 2008

Integrate Grails with Extjs

0/1 -> C -> C++ -> Java -> ? (Assume it is: Grails + extjs)

Extjs acts as front-end, grails works as back-end. Both Grails and Extjs can handle JSON very well. Grails has built-in support for JSON. This is not surprise that JSON will be communication tool between them.

However, in Grails official documentation, there is not enough statement for how to generate JSON. Here comes some of my example to create JSON with Grails (List, Create, Update, Delete), and this JSON data will be feed into extjs (i.e: extjs sexy part - grid).

Feeling: Grails saved me lots of time to write backend code - not need any config and I do not need write hibernate mapping, this is amazing. It really let developer focus on application business logic (Still remember 2000 when EJB come and said this - it really did not). Just Eclipse does not well support grails at this moment, hopefully it come soon.

See open source calendar app: www.cubedrive.com/myCalendar

0: Update with NetBeans IDE6.5 [Update Dec 24]

Recently I played this with NetBeans IDE6.5. Seems this one better than Eclipse, you just download the latest Netbeans package (250 M - huge). And Netbeans already have grails plugin. And you can simple click run button to make grails project run. Worth to have a try - I guess I will stay in Netbeans for a while in grails project.

1: Quick setup


2: list in Grail controller and extjs
/**
* Copyright (c) 2005 - 2008 FeyaSoft Corp. All Rights Reserved.
*/
import grails.converters.*

import com.feyasoft.myActivity.domain.account.*

/**
* @author Fenqiang Zhuang
* @Dec 1, 2008
*
* This file is used to define the account controller action
* This will provides the service for UI level.
* Most important actions are list, save, update, delete
*/
class AccountController {

// the delete, save and update actions only accept POST requests - maybe not necessary
def allowedMethods = [delete:'POST', update:'POST', create:'POST']

/**
* Mapping url: http://localhost:8080/fileMgr/account/list
*
* This will lost of accounts in the system based on search result.
* Return result will be in JSON format.
*
*/
def list = {
// log information
log.info(params: params)
if(!params.max) params.max = 20

// get a list of result based on params.
def accounts = Account.list(params)

// return a bunch of json data with metadata.
def json = [
metaData: [
totalProperty: 'totalCount',
root: 'results',
id: 'id',
fields: [
[name: "id", type: "int"],
[name: "firstname"],
[name: "lastname"],
[name: "username"],
[name: "email"]
]
],
totalCount: accounts.size,
results: accounts
]

render json as JSON
}

/**
* * Mapping url: http://localhost:8080/fileMgr/account/save
*
* create account method - get passed parameter from extjs and call save() function
* If everything ok, it will return success message for form.
*/
def create = {
def account = new Account(params)

// default error msg
def json = [failure : "true", errorInfo : "Internal Error, please try again"]

// check whether something wrong in save
if(!account.hasErrors()) {
// check username already exist or not
if (Account.findAllByUsername(account.username).size()>0) {
json = [failure : "true", errorInfo: "This username already existed, please choose another one"]
} else if (account.save()) {
json = [success : "true", info : "You have success creat this account"]
} else {
def errors = "Error is: "
account.errors.allErrors.each {
errors = errors + it
}
json = [failure : "true", errorInfo: errors]
}
}

// return json data
render json as JSON
}

/**
* This will load existing account information based on id from UI.
* Return back a json string.
*
* JSON like this:
*
* {"success":"true","data":[{"id":4,"class":"Account","createDate":new Date(1227909694000),
* "email":"","firstname":"jj","lastModifiedDate":new Date(1227909694000),"lastname":"j",
* "password":"kkkk","username":"llkl"}]}
*/
def load = {
def account = Account.get(params.id)

if(account) {
def json = [success: "true", data: [account]]
render json as JSON
} else {
// default error msg
def json = [failure : "true", errorInfo : "Internal Error, please try again"]
render json as JSON
}
}

/**
* This will update the passed parameter from UI level
* Called by the Edit.js file.
*/
def update = {
def account = Account.get( params.id )

// default error msg
def json = [failure : "true", errorInfo : "Internal Error, please try again"]

if(account) {
account.properties = params
if(!account.hasErrors()) {
if (account.save()){
json = [success : "true", info : "You have success update this account"]
} else {
def errors = "Error is: "
account.errors.allErrors.each {
errors = errors + it
}
json = [failure : "true", errorInfo: errors]
}
}
}

// return json data
render json as JSON
}

/**
* Delete this related account by id - passed by
*/
def delete = {
def json = [failure : "true", errorInfo : "Internal Error, please try again"]

// check whether del parameter is passed - if yes, delete first
if (params.delData != null) {
def jsonArray = JSON.parse(params.delData)
jsonArray.each {
log.info("start to delete account with id: ${it.id}")
def delAccount = Account.get(new Long("${it.id}"))
if (delAccount) {
delAccount.delete()
json = [success : "true", info : "You have success delete this account"]
}
}
}

render json as JSON
}
}
Here is the extjs side code:
    feyaSoft.account.List = function() {

Ext.QuickTips.init();

// define default rowsPerPage = 10;
var myPageSize = 20;

var accountCM = new Ext.grid.ColumnModel([
new Ext.grid.RowNumberer(),
{id: 'id', header: "Identify", dataIndex: 'id', width: 150, hidden: true},
{header: "First Name", dataIndex: 'firstname',width: 150},
{header: "Last Name", dataIndex: 'lastname',width: 150},
{header: "Username", dataIndex: 'username',width: 150},
{header: "Email", dataIndex: 'email',width: 150}
]);
accountCM.defaultSortable = false;

/************************************************************
* connect to backend - grid - core part
* create the Data Store
* connect with backend and list the result in page
* through JSON format
************************************************************/
this.dataStore = new Ext.data.JsonStore({
url: 'account/list',
remoteSort: true,
fields: [] // initialized from json metadata
});
this.dataStore.setDefaultSort('lastname', 'ASC');

/************************************************************
* Define menubar now in here
* add and delete functions
************************************************************/
var menubar = [{
text:'Add New Account',
tooltip:'Add a new account information',
iconCls:'addItem',
handler: function(){
// add new user now - call another JS class
new feyaSoft.account.Create();
}
},'-',{
text:'Archive',
tooltip:'Archive the selected items',
iconCls:'remove',
handler: function(){
//new feyaSoft.util.DeleteItem({panel: 'listAccount-panel'});
}
}];

var pagingbar = new Ext.ux.PagingToolbar({
pageSize: myPageSize,
store: this.dataStore,
displayInfo: true,
displayMsg: 'Displaying items {0} - {1} of {2}',
emptyMsg: "not result to display"
});

/************************************************************
* define grid panel now
* For more detail parameters, you can find from extjs API
************************************************************/
feyaSoft.account.List.superclass.constructor.call(this, {
id: 'list-account-panel',
ds: this.dataStore,
cm: accountCM,
viewConfig: {forceFit:true},
tbar: menubar,
bbar: pagingbar,
height:400,
width:800,
frame: true,
autoScroll:true
});

// trigger the data store load
this.dataStore.load({params:{start:0, limit:myPageSize}});

};

Ext.extend(feyaSoft.account.List, Ext.grid.GridPanel, {

// reload datasource
reload : function () {
this.dataStore.load();
},

remove : function (row) {
this.dataStore.remove(row);
},

deleteData : function (jsonData) {
this.dataStore.load({params:{start:0, delData:jsonData}});
}
});

Some hints maybe help for your to use Grails:

1: Get latest version Grail 1.0.4 and setup GRAIL_HOME in your ENV: http://grails.org/

2: Create a grails project through Eclipse.

3: Update

You do not need create tables - using dbCreate="create" for first time and grails will create DB table for us (you do need create database name). Go to:
grails-app/conf/DataSource.groovy file, you need update:
 dataSource {
pooled = true
driverClassName = "com.mysql.jdbc.Driver"
username = "root"
password = "XXXXXX"
}

development {
dataSource {
dbCreate = "update" // one of 'create', 'create-drop', 'update' - first time, please use 'create'
url = "jdbc:mysql://localhost:3306/demo?tcpKeepAlive=true&useUnicode=true&characterEncoding=UTF8"
}
}

4: Run

come to the project folder: cd yourProjectFolder // using cmd

> grails clean <--- sometimes compiled code are not cleaned and behavior weird - clean will help
> grails run-app

You can also run this through ant in Eclipse. However, I do not know how to stop Jetty under Eclipse (Finally I kill it through Ctrl + Alt + del and find task manager - kill all of Java running thread.).

4 comments:

Aaron Conran said...
This comment has been removed by the author.
Aaron Conran said...

Great Post!

I've recently been toying with Groovy and Grails and of course integrating it with Ext. :D

Anonymous said...

Spelling mistake: grails, rather than grail - someone may be misled.

Anonymous said...

Hi, very interesting post, greetings from Greece!