Basically you can follow the steps described in Peter Ledbrooks great blog:
Introducing SmartGWT to Grails
(Don't forget to place the SmartGWT jars under lib/gwt and let you IDE know where to find them).
That will get you up and running with a grails app and a SmartGWT frontend. I will try to show you how to get you data to and from the SmartGWT based on this little domain class and a corresponding service:
/**
* The domain.
*/
class Project {
String name
String title
String description
Boolean isPublic = Boolean.TRUE
static constraints = {
name(unique: true, blank: false)
title(blank: false)
description(nullable: true)
}
}
/**
* A little service.
*/
class ProjectService {
static transactional = true
Project findProject(Long id) {
Project.get(id)
}
def Project[] findProjects() {
Project.list()
}
def Project save(Map<String, String> parameters) {
log.info "save( ${parameters} )"
def theProject = Project.get(parameters.id)
if (!theProject) {
theProject = new Project()
}
theProject.properties = parameters;
theProject.isPublic =
"true".equals(parameters.isPublic) ? true : false
theProject.save()
}
def remove(Map<String, String> parameters) {
log.info "remove( ${parameters} )"
Project.get(parameters.id)?.delete()
}
}
Really nothing interesting here - but instead of creating GWT-RPC service interfaces, writing DTOs and calling this little service directly via async callbacks SmartGWT offers the concept of a Datasource. We'll use a RestDatasource to act as a "smart client" of this service, therefore we expose the service in a REST-ful manner. Here's my 5-minute Project controller (which may still be improved):
class ProjectController {
def projectService
def list = {
log.info "ProjectController.list( ${params} )"
def projects = projectService.findProjects();
def xml = new MarkupBuilder(response.writer)
xml.response() {
status(0)
data {
projects.each { theProject ->
flushProject xml, theProject
}
}
}
}
def save = {
log.info "ProjectController.add( ${params} )"
def theProject = projectService.save(params)
def xml = new MarkupBuilder(response.writer)
xml.response() {
status(0)
data {
flushProject xml, theProject
}
}
}
def remove = {
log.info "ProjectController.remove( ${params} )"
projectService.remove(params)
def xml = new MarkupBuilder(response.writer)
xml.response() {
data {
status(0)
record {
id(params.id)
}
}
}
}
private def flushProject = { xml, project ->
xml.record(
id: project.id,
name: project.name,
title: project.title,
description: project.description,
isPublic: project.isPublic
)
}
}
Again not much surprising stuff here: The controller simply calls the service methods and writes out the returned values as XML using the Groovy MarkupBuilder, so let's finally get to the interesting (SmartGWT-) issues.
After you created a GWT module and a GWT page as well as an entry point (remember the GWT plugin) define a (Singleton-) Datasource for the service as follows:
public class ProjectDs extends RestDataSource {
private static ProjectDs instance;
public static ProjectDs getInstance() {
if (instance == null) {
instance = new ProjectDs();
}
return instance;
}
private ProjectDs() {
//set id + general stuff
setID("projectDs");
setClientOnly(false);
//setup fields
DataSourceIntegerField idField =
new DataSourceIntegerField("id", "ID");
idField.setCanEdit(false);
idField.setPrimaryKey(true);
DataSourceTextField nameField =
new DataSourceTextField("name", "Name", 50, true);
TextItem nameItem = new TextItem();
nameItem.setWidth("100%");
nameField.setEditorType(nameItem);
DataSourceTextField titleField =
new DataSourceTextField("title", "Title", 50, true);
TextItem titleItem = new TextItem();
titleItem.setWidth("100%");
titleField.setEditorType(titleItem);
DataSourceTextField descField =
new DataSourceTextField("description", "Description", 500, false);
TextAreaItem areaItem = new TextAreaItem();
areaItem.setLength(500);
areaItem.setWidth("100%");
descField.setEditorType(areaItem);
DataSourceBooleanField isPublicField =
new DataSourceBooleanField("isPublic", "Public", 0, false);
setFields(idField, nameField, titleField, descField, isPublicField);
//setup operations
//1. fetch
OperationBinding fetch =
new OperationBinding(DSOperationType.FETCH, "/requesta/project/list");
fetch.setDataProtocol(DSProtocol.POSTPARAMS);
//2. update
OperationBinding update =
new OperationBinding(DSOperationType.UPDATE, "/requesta/project/save");
update.setDataProtocol(DSProtocol.POSTPARAMS);
//3. add
OperationBinding add =
new OperationBinding(DSOperationType.ADD, "/requesta/project/save");
add.setDataProtocol(DSProtocol.POSTPARAMS);
//4. remove
OperationBinding remove =
new OperationBinding(DSOperationType.REMOVE, "/requesta/project/remove");
remove.setDataProtocol(DSProtocol.POSTPARAMS);
setOperationBindings(fetch, update, add, remove);
}
}
- We extend the RestDatasource and create the fields to be shown in our views. (Note the call to idField.setPrimaryKey(true). If you forget to define the PK on the Datasource operations like 'remove' will fail to reflect correctly on the UI).
- Furthermore we tell the DS in what kind of UI components the field values should be rendered - that can be omitted or handled elsewhere.
- Last but not least we bind the good old CRUD-Operations to our simple Groovy controller/service
public class ProjectView extends HLayout implements ClickHandler {
private final ProjectDs projectDs;
private final ListGrid table = new ListGrid();
private final DetailViewer detail = new DetailViewer();
private final Window formWindow = new Window();
private final DynamicForm form = new DynamicForm();
private final IButton addButton = new IButton();
private final IButton saveButton = new IButton();
private final IButton removeButton = new IButton();
private final IButton cancelButton = new IButton();
public ProjectView(ProjectDs projectDs) {
this.projectDs = projectDs;
initUi();
}
private void initUi() {
//init myself
initMySelf();
//init listgrid
initGrid();
//a detail viewer
Layout detailsComp = initDetailViewer();
//form for editing
initEditForm();
//button layout
addMember(table);
addMember(detailsComp);
}
private void initMySelf() {
setWidth100();
setHeight100();
setMembersMargin(5);
setMargin(5);
setPadding(10);
}
private void initGrid() {
table.setDataSource(projectDs);
table.setDataPageSize(20);
table.setAutoFetchData(true);
table.setAlign(Alignment.CENTER);
table.setWidth("70%");
table.setWrapCells(true);
table.setEmptyCellValue("---");
table.addRecordClickHandler(new RecordClickHandler() {
public void onRecordClick(RecordClickEvent recordClickEvent) {
detail.viewSelectedData(table);
}
});
table.addRecordDoubleClickHandler(new RecordDoubleClickHandler() {
public void onRecordDoubleClick(RecordDoubleClickEvent recordDoubleClickEvent) {
form.editSelectedData(table);
formWindow.show();
}
});
}
private Layout initDetailViewer() {
detail.setGroupTitle("Project Details");
detail.setDataSource(projectDs);
detail.setShowEmptyMessage(true);
//add button
addButton.setTitle("ADD");
addButton.setTooltip("Create a new Project");
addButton.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent clickEvent) {
form.editNewRecord();
formWindow.show();
}
});
removeButton.setTitle("REMOVE");
removeButton.setTooltip("Delete this Project");
removeButton.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent clickEvent) {
table.removeSelectedData();
table.fetchData();
}
});
HLayout buttonPane = new HLayout();
buttonPane.setAlign(Alignment.CENTER);
buttonPane.addMember(addButton);
buttonPane.addMember(removeButton);
VLayout layout = new VLayout(10);
layout.addMember(detail);
layout.addMember(buttonPane);
return layout;
}
private void initEditForm() {
//the form
form.setIsGroup(false);
form.setDataSource(projectDs);
form.setCellPadding(5);
form.setWidth("100%");
saveButton.setTitle("SAVE");
saveButton.setTooltip("Save this Project instance");
saveButton.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent clickEvent) {
form.saveData();
formWindow.hide();
}
});
cancelButton.setTitle("CANCEL");
cancelButton.setTooltip("Cancel");
cancelButton.addClickHandler(this);
HLayout buttons = new HLayout(10);
buttons.setAlign(Alignment.CENTER);
buttons.addMember(cancelButton);
buttons.addMember(saveButton);
VLayout dialog = new VLayout(10);
dialog.setPadding(10);
dialog.addMember(form);
dialog.addMember(buttons);
//form dialog
formWindow.setShowShadow(true);
formWindow.setShowTitle(false);
formWindow.setIsModal(true);
formWindow.setPadding(20);
formWindow.setWidth(500);
formWindow.setHeight(350);
formWindow.setShowMinimizeButton(false);
formWindow.setShowCloseButton(true);
formWindow.setShowModalMask(true);
formWindow.centerInPage();
HeaderControl closeControl = new HeaderControl(HeaderControl.CLOSE, this);
formWindow.setHeaderControls(HeaderControls.HEADER_LABEL, closeControl);
formWindow.addItem(dialog);
}
public void onClick(ClickEvent clickEvent) {
formWindow.hide();
}
}
A few things to notice
- The way we query our Datasource - we don't, we just let the ListGrid do the dirty work :)
- The way we construct the form - we don't, it just gets our datasource and creates itself :) (Same applies to the DetailViewer)
- The way the components interact - again interaction is not "direct" but uses the Datasource to pass around records ( "form.editSelectedData(table);" )
- The rest is just layouting an eye-candy
Here's the entry point
public class App implements EntryPoint {
/**
* This is the entry point method.
*/
public void onModuleLoad() {
ProjectDs projectDs = ProjectDs.getInstance();
ProjectView projectView = new ProjectView(projectDs);
projectView.draw();
}
}
And all that's left do do is
- grails run-app (to start the service)
- grails compile-gwt-modules (may take a while)
- grails run-gwt-client
Hope this little post sheds some light on how one could use SmartGWT with Grails - any comments appreciated...