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...
hey, post was really helpful.It helped me a lot as i didn't find any example that dealt with client server interaction in the SmartGWT showcase examples. I just have one question..Are the styles customizable for the SmartGWT widgtes? Can i apply my css styles and change the look and feel of the widgets?
AntwortenLöschenExcellent post Josip!
AntwortenLöschenRegarding the styles, yes, they are highly customizable with simple modifications to skin_styles.css. For more advanced theme customizations you can alter load_skins.js.
There are several themes provided with SmartGWT - Enterprise (the one used here), Enterprise Blue, and Graphite. You can check them out by using the theme changer drop down at the top right of the showcase demo : http://www.smartclient.com/smartgwt/showcase/#main
Here's another example of an extremely lightweight theme that uses minimal media and takes advantage of CSS3 as well :
http://www.smartclient.com/smartgwt/playpen/#featured_tree_grid
If you use in host mode there is a problem (JS alert error).
AntwortenLöschenhttp://localhost:8080/myapp/app.gsp?gwt.codesvr=127.0.0.1:9997
Error: Cannot change the configuration property 'headerControls'...
But if you access http://localhost:8080/myapp/app.gsp. It's works
Why?
The full stack
AntwortenLöschen00:15:38,530 [ERROR] Unable to load module entry point class org.example.client.App (see associated exception for details)
java.lang.IllegalStateException: Cannot change configuration property 'headerControls' to [Ljava.lang.Object;@4cd904 after the component has been created. at com.smartgwt.client.widgets.BaseWidget.error(BaseWidget.java:540) at com.smartgwt.client.widgets.BaseWidget.error(BaseWidget.java:528) at com.smartgwt.client.widgets.BaseWidget.setAttribute(BaseWidget.java:764) at com.smartgwt.client.widgets.Window.setHeaderControls(Window.java:1391) at org.example.client.ProjectView.initEditForm(ProjectView.java:150) at org.example.client.ProjectView.initUi(ProjectView.java:45) at org.example.client.ProjectView.(ProjectView.java:34) at org.example.client.App.onModuleLoad(App.java:12) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at com.google.gwt.dev.shell.ModuleSpace.onLoad(ModuleSpace.java:369) at com.google.gwt.dev.shell.OophmSessionHandler.loadModule(OophmSessionHandler.java:185) at com.google.gwt.dev.shell.BrowserChannelServer.processConnection(BrowserChannelServer.java:380) at com.google.gwt.dev.shell.BrowserChannelServer.run(BrowserChannelServer.java:222) at java.lang.Thread.run(Thread.java:619)
@Sanjiv: Thanks :)
AntwortenLöschen@Anonym: Strange error - haven't seen it yet!
Could it be that you are missing a Grails controller for the "myapp/app.gsp"? I left out the controller in the post as it is empty and does nothing but provide an emtpy 'def index= {}' closure. - Just a guess, but if the page works calling it directly (with the .gsp) you could try using such an empty controller...
I'm currenlty working to offer more information about SMartGWT/GRails integration.
AntwortenLöschenSee:
* www.grails.org/plugin/smartgwt
* code.google.com/p/grails-gwt-smart/
I'm really intested to integration of your tutorial. Because, my tuto are little bit limited.
follow me on twitter:jvmvik
Thank you this was a real help.
AntwortenLöschenThanks for this great tutorial. In host mode I get the same error message 'Error: Cannot change the configuration property 'headerControls'...'. Any insight into the cause of this?
AntwortenLöschenPeter
ok, got it: In the function initEditForm() make sure that
AntwortenLöschenformWindow.setHeaderControls(HeaderControls.HEADER_LABEL, closeControl);
first thing before any of the other formWindow.set(..) methods are called.
Great tutorial. I'm able to added projects but when it tries to retrieves the projects to populate the ListGrid it get a 404 when trying to access /projectName/project/list. If you can shed some light it would be much appreciated.
AntwortenLöschenThanks!!!
what a great post you made, thank you Josip,I have some felling about Datasource and binding of smartgwt and how to get all these done together with grails now, with your great tutorial, I prevent wasting another 1 week trying to find something from the chaos
AntwortenLöschengr8 post Josip...
AntwortenLöschenJust wanted to know... at what place would I put the "(Singleton-) Datasource for the service" .. in
src/java or src/gwt under 'client' package or some where else ..?
Hey Josip,
AntwortenLöschengr8 tutorial, we'd much appreciate if you can also share the source code of the example.
+1 Anonym,
AntwortenLöschenhi Josip... I believe this is perfect fonxr someone who has the basic idea of how grails and smartGWT work. but a novice like me would really appreciate a packaging structure screen shot or some class placment guidance....
thanx
How to handle relationships between domain classes to save data when using SmartGWT and grails?
AntwortenLöschen