Skip to main content

Integrate XForms with the Google Web Toolkit, Part 4: Creating interactive forms with GWT and XForms

Create a Web application for rock star hopefuls

Michael Galpin (mike.sr@gmail.com), Software Engineer, Ludi Labs
Michael Galpin has been developing Java software professionally since 1998. He currently works at Ludi Labs, a start-up in Mountain View, Calif. He holds a degree in mathematics from the California Institute of Technology.

Summary:  This four-part series demonstrates how to use the Google Web Toolkit (GWT) and XForms together to create a dynamic Web application. Part 1 looks at the JavaScript underpinnings of each technology. Part 2 shows how to use those JavaScript underpinnings to start mixing the two technologies together to build the rock star application. Part 3 refactors the application to use XForms and GWT together. In this concluding part, you'll continue to refactor and improve your rock star application.

View more content in this series

Date:  16 Oct 2007
Level:  Intermediate
Activity:  3199 views

Introduction

Get up to speed with the four-part series Integrate XForms with the Google Web Toolkit:
  • Part 1: Introducing GWT's JavaScript Native Interface
  • Part 2: Create an artist and album management form
  • Part 3: Use GWT to create XForms

In this final installment of the series, you'll add an interactive form for adding new albums for an artist. This will use GWT Ajax, JSNI, and XForms controls. You'll look at how GWT can augment XForms. To demonstrate this, you'll look at how you can use GWT to provide localized content for our XForms controls. Finally, you'll take a look at a not-so-well-known feature of GWT, Java™-style sorting, that provides yet another example of how GWT can make a Java developer's job a little easier.


Prerequisites

This article uses GWT version 1.4 and the Mozilla XForms plugin 0.8 (see the Resources for download links). The Mozilla XForms plugin works with any Mozilla-based Web browser, such as Firefox and Seamonkey. GWT requires knowledge of Java technology, and Web technologies such as HTML and CSS. This article makes heavy use of JavaScript as well. XForms makes heavy use of the Model-View-Control paradigm, so familiarity with that is helpful. Prior exposure to XForms and GWT is helpful of course, but is not necessary.


Creating an interactive XForm

You want to create a simple data entry form. It will have two fields, the title of the album and the year it was recorded. You'll have a button that will trigger your remote call. XForms makes it easy to create a UI like using its controls, as shown in Listing 1.


Listing 1. Album entry form using XForms controls
                
<xhtml:div id="albumForm">
         <xforms:input id="title">
              <xforms:label>Title:</xforms:label>
         </xforms:input>
         <xforms:input id="year">
              <xforms:label>Year:</xforms:label>
         </xforms:input>
         <xforms:trigger id="btn">
              <xforms:label>Add Album</xforms:label>
              <xforms:load resource="javascript:addAlbum()" 
			  ev:event="DOMActivate"/>
         </xforms:trigger>
</xhtml:div>

It should be no surprise that XForms makes it very easy to create a form. The only thing tricky going on here is that your trigger is calling JavaScript. Normally an XForms trigger calls a submission action defined on the XForms model. You are going to use GWT to make an Ajax request to a remote service, so your trigger is calling JavaScript instead. Let's take a look at the JavaScript for calling the remote service.


Calling a remote service from an XForm

In Part 3 you defined a service (AlbumService) that included a method for adding a new album. That's the service you are going to call. Looking at the signature of that interface, you'll need to create a new Album object to send to the server. See the class in Listing 2.


Listing 2. Album class
                
package org.developerworks.rockstar.client;

import com.google.gwt.user.client.rpc.IsSerializable;

public class Album implements IsSerializable{
     private int id;
     private int artistId;
     private String title;
     private int year;
     
     public int getId() {
          return id;
     }
     public void setId(int id) {
          this.id = id;
     }
     public int getArtistId() {
          return artistId;
     }
     public void setArtistId(int artistId) {
          this.artistId = artistId;
     }
     public String getTitle() {
          return title;
     }
     public void setTitle(String title) {
          this.title = title;
     }
     public int getYear() {
          return year;
     }
     public void setYear(int year) {
          this.year = year;
     }
     
}

One of the things you should notice in this class is that it has the ID of the artist. So when you create a new album, you'll need the ID of the artist. This is passed in as a request parameter to the page. You'll need some way to store this information and make it available to the your script that is going to invoke your remote service. There are a number of ways to do this, but one nice technique you can use with XForms is to store it as part of a model, as shown in Listing 3.


Listing 3. Storing request parameter in XForms model
                
<xforms:model id="request">
    <xforms:instance id="artist" xmlns="">
        <Data>
             <ArtistId><%=request.getParameter("artistId") 
			 %></ArtistId>
        </Data>
    </xforms:instance>
</xforms:model>

You could imagine putting other various request parameters as part of the model instance data. Now let's take a look at the addAlbum() you referenced in the XForms trigger. Since you are using GWT, this will just be another method in the AlbumLib, as shown in Listing 4.


Listing 4. The addAlbum() method
                
public void addAlbum(){
     String title = this.getAlbumTitle();
     int year = Integer.parseInt(this.getAlbumYear());
     int artistId = Integer.parseInt(this.getArtistId());
     Album album = new Album();
     album.setArtistId(artistId);
     album.setTitle(title);
     album.setYear(year);
     AlbumServiceAsync albumService = this.getAlbumService();
     AsyncCallback callback = new AsyncCallback(){

          public void onFailure(Throwable caught) {
               removeAlbum();
               refreshXformsModel();
          }

          public void onSuccess(Object result) {
               // added optimistically, so nothing to do here
          }
               
     };
     albumService.addAlbum(album, callback);
     this.addAlbumToModel(album);
     this.refreshXformsModel();
}

The method is fairly straightforward. You create a new album, get a reference to the album service, and then invoke that service's addAlbum() method. There are several other methods are using here, so let's take a look at those too in Listing 5.


Listing 5. Helper methods for addAlbum()
                
private native String getAlbumTitle()/*-{
     return $doc.getElementById("title").value;
}-*/;
     
private native String getAlbumYear()/*-{
     return $doc.getElementById("year").value;
}-*/;
     
private native String getArtistId()/*-{
     var model = $doc.getElementById("request");
     var instance = model.getInstanceDocument("artist");
     var dataElement = instance.getElementsByTagName("Data")[0];
     return dataElement.getElementsByTagName("ArtistId")[0].firstChild.getTextValue();
}-*/;
     
private native void removeAlbum()/*-{
     var model = $doc.getElementById("albums");
     var instance = model.getInstanceDocument("albumData");
     var dataElement = instance.getElementsByTagName("Data")[0];
     dataElement.removeChild(dataElement.lastChild);
}-*/;

All of these are native JavaScript methods created using GWT's JSNI facility. The getAlbumTitle() and getAlbumYear() are both just using JavaScript DOM methods to get the values from the form elements. The getArtistId() method uses the XForms JavaScript APIs to access the artist ID stored in the model shown in Listing 3. Finally, the removeAlbum() method also uses the XForms JavaScript APIs to remove the last album from the XForms model. Looking back at Listing 4, you see that removeAlbum() is called when your remote service invocation fails. That's because the addAlbum() method in Listing 4 optimistically adds the new album to the model and refreshes the UI controls that are bound to the model.

Note: The other two methods called by addAlbum(), the addAlbumToModel() and refreshXformsModel() methods, can be seen in the previous article, as well as with the source code for this article.


Creating the XForms controls with GWT JSNI

In the previous article, you saw that you can use GWT and JSNI to create XForms controls. You've defined some new controls for your data entry form, but you can once again make use of GWT and JSNI to create these new controls. You'll define a new method for this called createEntryForm(), which is shown in Listing 6.


Listing 6. The createEntryForm() method
                
private native void createEntryForm()/*-{
     var xfNs = "http://www.w3.org/2002/xforms";
     // get the container div
     var container = $doc.getElementById("albumForm");
         
     var titleIn = $doc.createElementNS(xfNs, "xforms:input");
    titleIn.setAttribute("id","title");
    var titleInLabel = $doc.createElementNS(xfNs, "xforms:label");
    titleInLabel.appendChild($doc.createTextNode("Year:"));
    titleIn.appendChild(titleInLabel);
    container.appendChild(titleIn);
         
    var yearIn = $doc.createElementNS(xfNs, "xforms:input");
    yearIn.setAttribute("id","year");
    var yearInLabel = $doc.createElementNS(xfNs, "xforms:label");
    yearInLabel.appendChild($doc.createTextNode("Year:"));
    yearIn.appendChild(yearInLabel);
    container.appendChild(yearIn);
         
    var trigger = $doc.createElementNS(xfNs, "xforms:trigger");
    trigger.setAttribute("id", "btn");
    var btnLbl = $doc.createElementNS(xfNs, "xforms:label");
    btnLbl.appendChild($doc.createTextNode("Add Album"));
    trigger.appendChild(btnLbl);
    var loader = $doc.createElementNS(xfNs, "xforms:load");
    loader.setAttribute("resource", "javascript:addAlbum()");
    loader.setAttributeNS("http://www.w3.org/2001/xml-events", "ev:event", "DOMActivate");
    trigger.appendChild(loader);
    container.appendChild(trigger);
}-*/;

This method is very straightforward. You simply create the various XForms controls using normal JavaScript DOM APIs, and add this to your DOM tree. In this case, you create the two inputs and the trigger. You have to make sure to use the namespace-aware versions of things like createElement and setAttribute. This is because XForms uses its own namespaces to augment the XHTML schema. Now all you need to do is add a reference to this method in your startup script, and you're ready to start testing the application.


Testing the application

For testing the application, you'll need to once again use Web mode. You can launch the application in hosted mode, and then launch Web mode, as shown in Figure 1.


Figure 1. Launching Web mode
launching Web mode

This will bring up the application in Web Mode. You can click on one of the artists now to bring up the albums page, as shown in Figure 2.


Figure 2. The albums page in Web mode
The albums page in Web mode

Add the new title and year and click the Add Album button. The interface should update, as shown in Figure 3.


Figure 3. New album added
New album added

Now you are using GWT and XForms together to create the UI and create dynamic interactions with back-end services. Let's take a look at some of the other benefits of using these two technologies together, now that you've seen how to combine them.


Using GWT localization with XForms

GWT is designed to be a multi-purpose framework for building Web applications. One of the common problems with Web applications is localization. Google has a presence in many countries, so localization is a problem they should be very familiar with. Therefore, it should come as no surprise that GWT has some clever features designed for localization. XForms is a much more focused technology, so something like localization is not part of its scope. Thus GWT's localization can be very useful for an XForms application.

Localization with GWT: The Constants interface

When it comes to localization, many Java technology developers only use one thing: resource bundles. Those are useful, but limited. GWT uses an implied interface pattern. There is a Java interface corresponding to a properties file. This creates a compile-time contract between localized content and Java code, including GWT code. Let's take a look at using this technique with your application.

Your albums page has five main pieces of content: the title label, the year label, the title input label, the year input label, and the label on the Add button. This defines a Java interface, as shown in Listing 7.


Listing 7. Java interface for albums content
                
package org.developerworks.rockstar.client;

import com.google.gwt.i18n.client.Constants;

public interface AlbumContent extends Constants{
     public String titleLabel();
     public String yearLabel();
     public String titleInputLabel();
     public String yearInputLabel();
     public String addButtonLabel();
}

Now you can create a properties file corresponding to this interface. Your interface is named AlbumContent, so the file must be called AlbumContent.properties and must be in the same package as the AlbumContent interface. The file is shown in Listing 8.


Listing 8. AlbumContent.properties
                
titleLabel=Title:
yearLabel=Year:
titleInputLabel=Title:
yearInputLabel=Year:
addButtonLabel=Add Album

The names of the properties in this file must correspond to the methods defined in your interface. Listing 8 shows one version of the properties file. You can then have this translated into as many languages as needed and add them to the same location. At runtime, GWT will determine the locale and you can get a localized implementation of the interface. This can be called from client-side Java methods, and thus also from native JavaScript methods defined using JSNI. Let's take a look at this technique in Listing 9.


Listing 9. Accessing localized content
                
private AlbumContent getContent(){
     return (AlbumContent) GWT.create(AlbumContent.class);
}

private native void createEntryForm()/*-{
      // get the localized content
     var content = this.@org.developerworks.rockstar.client.AlbumLib::getContent();
// ...
    var titleInLabel = $doc.createElementNS(xfNs, "xforms:label");
    var titleLabelTxt = content.titleInputLabel();
    titleInLabel.appendChild($doc.createTextNode(titleInputTxt));
// ...         
    var yearInLabel = $doc.createElementNS(xfNs, "xforms:label");
    var yearLabelTxt = content.yearInputLabel();
    yearInLabel.appendChild($doc.createTextNode(yearLabelTxt));
// ...
    var btnLbl = $doc.createElementNS(xfNs, "xforms:label");
    var btnLblTxt = content.addButtonLabel();
    btnLbl.appendChild($doc.createTextNode("Add Album"));

}-*/;

Listing 9 shows a convenience method for accessing localized content. It then shows a new version of the createEntryForm you created earlier. This time you're grabbing the localized strings from your localized implementation of the interface. Only the code dealing with the localized content is shown, with the non-relevant code omitted in the listing. The full source code is available for download. This is a simple example of GWT's localization. It can handle more sophisticated localized content, such as content with placeholders.


The rock star contest: More hidden GWT gems

You've spent a lot of time on the albums' page of your application, combining the capabilities of GWT and XForms. Now let's go back to the artist page and add some more functionality to it. Let's allow your record executives to vote for one of the artists. You'll add a votes field to our XML and to your Artist class. You'll also add a vote feature to your ArtistService. Here's what the implementation on the server will look like, as shown in Listing 10.


Listing 10. Implementation of voteForArtist
                
public void voteForArtist(int artistId) {
     Artist artist = null;
     for (Artist a : this.artists){
          if (a.getId() == artistId){
               artist = a;
               break;
          }
     }
     if (artist != null){
          int votes = artist.getVotes() + 1;
          artist.setVotes(votes);
     }
     dao.saveArtists(this.artists);
}

You simply find the artist, increment its votes, and save the list. This is definitely not the most efficient code, but good enough for your purpose. Next you'll modify your code for your table of artists, as shown in Listing 11.


Listing 11. Adding voter buttons to the artist table
                
private void populateTable(){
      // clear the table
       int rowCount = this.artistTable.getRowCount();
       for (int i=0;i<rowCount;i++){
            this.artistTable.removeRow(i);
       }
      // create the header
      this.artistTable.getRowFormatter().addStyleName(0, "tableHeader");
      this.artistTable.setText(0, 0, "Name");
      this.artistTable.setText(0, 1, "Genre");
      this.artistTable.setText(0,2, "Vote Now!");
      // now add artists
      for (int i=0;i<artistList.size();i++){
           //this.artistTable.setText(i+1, 0, artists[i].getName());
           final Artist artist = (Artist) artistList.get(i);
           String html = "<a 
href=\"Albums.jsp?artistId="+artist.getId()+"\">"+artist.getName()+"</a>";
           this.artistTable.setHTML(i+1, 0, html);
           this.artistTable.setText(i+1, 1, artist.getGenre());
           final Button btn = new Button("Vote for:" + artist.getName());
           
           ClickListener cl = new ClickListener(){

               public void onClick(Widget sender) {
                    voteForArtist(artist);
               }
                
           };
           btn.addClickListener(cl);
           this.artistTable.setWidget(i+1, 2, btn);
      }
      this.artistTable.setBorderWidth(4);
  }

Notice that you added a Vote button next to each artist in your table. You're calling a voteForArtist() method, which is shown in Listing 12.


Listing 12. The voteForArtist() method
                
private void voteForArtist(final Artist artist){
      ArtistServiceAsync artistService = this.getArtistService();
       AsyncCallback callback = new AsyncCallback(){

          public void onFailure(Throwable caught) {
               artist.setVotes(artist.getVotes() - 1);
          }

          public void onSuccess(Object result) {
               // Nothing to do here
          }
            
       };
       artistService.voteForArtist(artist.getId(), callback);
       artist.setVotes(artist.getVotes() + 1);
  }
  

Now you just need a way to show the results of the voting. You'll create a grid for this. You'll use a grid instead of a FlexTable since you know how many artists there are when you create the grid. You'll create a button for showing or hiding the leaderboard, as shown in Listing 13.


Listing 13. Button for showing or hiding the leaderboard
                
final Panel lbPanel = new VerticalPanel();
final Button leaderButton = new Button("Show Leaderboard");
public void onModuleLoad() {
     // add the outer panel, then add to it
     RootPanel.get().add(outerPanel);
     outerPanel.add(artistTable);
     outerPanel.add(formPanel);
     outerPanel.add(leaderButton);
     outerPanel.add(lbPanel);
// ... More code omitted for brevity
     ClickListener lbListener = new ClickListener(){

          public void onClick(Widget sender) {
               if (leaderButton.getText().equals("Show Leaderboard")){
                    showLeaderBoard();
                    leaderButton.setText("Hide Leaderboard");
               } else {
                    lbPanel.clear();
                    leaderButton.setText("Show Leaderboard");
               }
          }
          
     };
     leaderButton.addClickListener(lbListener);
    // ...
  }
  

You'll go back and forth between showing and hiding the leaderboard, and changing the label on the button to match the state. Now you just need to draw the leaderboard, as shown in Listing 14.


Listing 14. Show leaderboard
                
private void showLeaderBoard(){
       int size = this.artistTable.getRowCount() ;
       Grid leaderBoard = new Grid(size,2);
       leaderBoard.getRowFormatter().addStyleName(0, "tableHeader");
       leaderBoard.setText(0, 0, "Aritst");
       leaderBoard.setText(0, 1, "Votes");
       // sort the artists
       List sorted = sortArtists();
       for (int i=0;i<size-1;i++){
            Artist artist = (Artist) sorted.get(i);
            leaderBoard.setText(i+1, 0, artist.getName());
            leaderBoard.setText(i+1,1, ""+artist.getVotes());
       }
       lbPanel.add(leaderBoard);
  }

Everything has been straighforward and the GWT has been "vanilla" so far, and you might be wondering why you're bothering to show this here. Here's where things get interesting. Notice you have a mysterious call to a sortArtists() method. You see, your list of artists is not inherently sorted, and of course you want it sorted for the leaderboard. So should you go back to the server to sort the list? Obviously it would be better to do it on the client. That means writing a sort in JavaScript, right? Let's take a look at how to do this GWT style, as shown in Listing 15.


Listing 15. Sorting the artists
                
private List sortArtists(){
       List sorted = new ArrayList(artistList);
       Collections.sort(sorted, new ArtistComparator());
       return sorted;
  }
  
  static class ArtistComparator implements Comparator{

     public int compare(Object arg0, Object arg1) {
          Artist a0 = (Artist) arg0;
          Artist a1 = (Artist) arg1;
          return a1.getVotes() - a0.getVotes();
     }
       
  }

You've sorted the list the way we do it in Java. You created a Comparator and used the JDK's Collections.sort() method. The JDK documentation indicates that this method gives you a modified mergesort with guaranteed N*log(N) performance. Would you want to write a mergesort in JavaScript? Well, you don't have to with GWT. Now let's test your application, as shown in Figure 4.


Figure 4. Hidden leaderboard
Hidden leaderboard

Just click on the Show Leaderboard button to bring up the leaderboard, as shown in Figure 5.


Figure 5. Show leaderboard
Show leaderboard

Looks like Dojo Darling could use some more votes. Click on its button, then Hide/Show the leaderboard to refresh it, as shown in Figure 6.


Figure 6. Updated leaderboard
Updated leaderboard

Summary

In this final installment in the GWT and XForms series, you saw how to add interactive forms to your application. These forms can use GWT to create XForms controls that can then invoke GWT Ajax services. The response from these services can be handled through GWT, and can in turn use JSNI to alter your XForms model data and update your XForms UI controls. This tight integration between GWT and XForms allows XForms to take advantage of key features of GWT. You also saw that you can localize your XForms using GWT's localization facilities. Finally, you saw one of the lesser-known, but powerful, features of GWT: Java-style sorting. Features like this, as well features like localization and GWT's new image bundling, make it so beneficial to use GWT not only for new projects, but also to enhance other projects and technologies.



Download

DescriptionNameSizeDownload method
Part 4 sample coderockstar4_src.zip16KB HTTP

Information about download methods


Resources

Learn

Get products and technologies

Discuss

About the author

Michael Galpin has been developing Java software professionally since 1998. He currently works at Ludi Labs, a start-up in Mountain View, Calif. He holds a degree in mathematics from the California Institute of Technology.

Comments (Undergoing maintenance)



Trademarks  |  My developerWorks terms and conditions

Help: Update or add to My dW interests

What's this?

This little timesaver lets you update your My developerWorks profile with just one click! The general subject of this content (AIX and UNIX, Information Management, Lotus, Rational, Tivoli, WebSphere, Java, Linux, Open source, SOA and Web services, Web development, or XML) will be added to the interests section of your profile, if it's not there already. You only need to be logged in to My developerWorks.

And what's the point of adding your interests to your profile? That's how you find other users with the same interests as yours, and see what they're reading and contributing to the community. Your interests also help us recommend relevant developerWorks content to you.

View your My developerWorks profile

Return from help

Help: Remove from My dW interests

What's this?

Removing this interest does not alter your profile, but rather removes this piece of content from a list of all content for which you've indicated interest. In a future enhancement to My developerWorks, you'll be able to see a record of that content.

View your My developerWorks profile

Return from help

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=XML, Java technology
ArticleID=261843
ArticleTitle=Integrate XForms with the Google Web Toolkit, Part 4: Creating interactive forms with GWT and XForms
publish-date=10162007
author1-email=mike.sr@gmail.com
author1-email-cc=ruterbo@us.ibm.com

My developerWorks community

Tags

Help
Use the search field to find all types of content in My developerWorks with that tag.

Use the slider bar to see more or fewer tags.

Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere).

My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Use the search field to find all types of content in My developerWorks with that tag. Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Special offers