Monday, April 23, 2012

GWT Custom CellTable Column

I love GWT's Cell widgets. For example the CellTable is such a leap forward from displaying information than their stupid-ass FlexTable.  They do have some drawbacks, like not being able to bind click events of individual cells in the presenter which would be good when you want to have multiple buttons in each row.  And the other drawback is, where the hell is the documentation for creating a custom column in a CellTable? I'm looking at you Google!  So without an examples or documentation we are left to muddle through the source code, which might be fun to you but I have a wife who wants to spend time with me and I got shit to do.  Well now she's upset because I'm not giving her the emotional attention she requires, the dishes are all dirty, half the fish are starving and the other are floating upside-down, my kid's diaper is overflowing, I missed the Game of Thrones premier, and I had to get an extension on filing my taxes, dogs and cats living together! Mass hysteria!

It's actually not that hard to figure out but luckily I'm here to save you some time.  So let's say we have a custom widget that we would like to use in a CellTable.  In this example I will be using my StarRating widget from a previous post which you can find here: http://programmingfortherestofus.blogspot.com/2012/04/gwt-star-rating-widget-with-half-stars.html

First step we need to take is to create a class that extends AbstractCell and implements Cell so the CellTable knows how to draw the cell and what data type it will expect, as well as overrides the render method where we add our widget's HTML to an HtmlBuilder. And if we are allowing user interaction we first pass what events we want to capture to the super class, for example mouse click and the enter key press, and then we override the onBrowserEvent, onEnterKeyDown, and any other events we wanted to capture.

StarRatingCell.java
/**
 * 
 */
package com.yourproject.widgets;

import com.google.gwt.cell.client.AbstractCell;
import com.google.gwt.cell.client.Cell;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;

/**
 * @author Austin_Rappa
 *
 */
public class StarRatingCell extends AbstractCell<StarRating> implements Cell<StarRating> {

    
    /**
     * Cell that contains a StarRating widget
     */
    public StarRatingCell() {
        super("click", "keydown");
    }


    
    /**
     * Render a cell as HTML into a SafeHtmlBuilder, suitable for passing to Element.setInnerHTML(String) on a container element. 
     */
    @Override
    public void render(com.google.gwt.cell.client.Cell.Context context, StarRating value, SafeHtmlBuilder sb) {
          sb.appendHtmlConstant("<div style=\"padding:0px;\">");
          if (value != null) {
              sb.appendHtmlConstant(value.toString());
          }
          sb.appendHtmlConstant("</div>");        
    }


    /**
     * Called when an event occurs in a rendered instance of this Cell. The     
     * parent element refers to the element that contains the rendered cell, NOT     
     * to the outermost element that the Cell rendered.     
     */    
    @Override    
    public void onBrowserEvent(Context context, Element parent, StarRating value, NativeEvent event, ValueUpdater<StarRating> valueUpdater) {               
        super.onBrowserEvent(context, parent, value, event, valueUpdater);         
        
        // Handle the click event.             
        if ("click".equals(event.getType())) {             
            // Ignore clicks that occur outside of the outermost element.            
            EventTarget eventTarget = event.getEventTarget();             
            if (parent.isOrHasChild(Element.as(eventTarget))) {                                
                // use this to get the selected element!!                
                Element el = Element.as(eventTarget);                 
                
                //Get the value, if it is clear then set it to zero
                if (el.getAttribute("title").equals("clear")) {
                    value.setRating(0);                    
                }
                else {
                    value.setRating(Integer.parseInt(el.getAttribute("title")));
                }
                 

                //Set the value of the StarRating
                setValue(context, parent, value);                                              
            }        
        }     
    };
        
                    

    /**     
     * onEnterKeyDown is called when the user presses the ENTER key will the     
     * Cell is selected. You are not required to override this method, but its a     
     * common convention that allows your cell to respond to key events.     
     */    
    @Override    
    protected void onEnterKeyDown(Context context, Element parent, StarRating value, NativeEvent event, ValueUpdater<StarRating> valueUpdater) {
        setValue(context, parent, value);                               
    }
}


I mean how much easier can it get?  Well I would like to introduce you to the next step, to create the column class that holds our StarRatingCell and overrides the getValue method to return a StarRating object. 

StarRatingColumn.java
package com.yourproject.widgets;

import com.google.gwt.user.cellview.client.Column;
    
/**
 * A column that displays its contents with a {@link StarRating} and does not make use of view data.
 *
 * @param <T> the row type
 * @author Austin_Rappa
 */
public abstract class StarRatingColumn<T> extends Column<T, StarRating> {

    /**
     * Construct a new StarRatingColumn.
     */
    public StarRatingColumn() {
        super(new StarRatingCell());
    }

    
     /**      
      * Return the passed-in object. 
      * @param object The value to get
      */    
    @Override    
    public StarRating getValue(T object) {    
        return null;         
    }  
}


And with this we are all ready to start puting this into a CellTable!  We instantiate the StarRatingColumn and define the type that contains our rating value, then override the getValue method, and finally set a field updater when allowing user interaction to update the html in the table when the value changes.

CellTable<ThingIWantToRate> cellTable = new CellTable<ThingIWantToRate>();
    
...
    
StarRatingColumn<ThingIWantToRate> ratingColumn = new StarRatingColumn<ThingIWantToRate>() {
    @Override    
    public StarRating getValue(ThingIWantToRate object) {
        return new StarRating(object.getRating(), 10, false);    
    }  
};
cellTable.addColumn(ratingColumn, "Rating");

//Update view
ratingColumn.setFieldUpdater(new FieldUpdater<ThingIWantToRate, StarRating>() {
    public void update(int index, ThingIWantToRate object, StarRating value) {
        //Set cell's value
        object.setRating(value.getRating());
                    
        //TODO: Update database here
                    
        //Redraw table
        cellTable.redraw();                    
    }                
});

3 comments:

  1. This doesn't seem to work. The widget works great on it's own, but once inside a CellTable, the mouse events no longer seem to reach the widget. No mouseover, no click. Is there a way to pass these events from the the cell to the widget? I've tried using GWT 2.3 and 2.4.

    Thanks,

    Robert

    ReplyDelete
    Replies
    1. Good catch Robert. All we would have to do is tell our AbstractCell super class what browser events we would like to handle then add the methods to handle those browser events. I've updated the code above as well as a slight update to the StarRating in my previous post.

      Thanks!

      Delete
  2. This comment has been removed by the author.

    ReplyDelete