Friday, November 4, 2011

Custom GWT clickable cell with multiple images for CellTable

I wrote already similar article here, which describes very well how to use images into custom created cell. Also there is a very nice tutorial describing how to create custom cells you can read from the official GWT documentation here. I’ve been asked from a visitor the my blog about use case where we do have more then one images into the custom cell and then he wants to know which image was clicked. It is actually very good question, so I was playing around and my example looks now like this:

image

If I click on one of those icons I want to know which icon was clicked.

To start you can create custom cell like described into the GWT tutorial from the official page, nothing really special here. The difference, I am extending the AbstractSafeHtmlCell as I did in my previously article. So my class look like this:

import com.google.gwt.cell.client.AbstractSafeHtmlCell;
import com.google.gwt.cell.client.ValueUpdater;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.safecss.shared.SafeStyles;
import com.google.gwt.safecss.shared.SafeStylesUtils;
import com.google.gwt.safehtml.client.SafeHtmlTemplates;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.text.shared.SafeHtmlRenderer;
import com.google.gwt.text.shared.SimpleSafeHtmlRenderer;
import com.google.gwt.user.client.ui.AbstractImagePrototype;
import com.gwt2go.dev.client.ui.icons.DocIcons;
 
/**
 * Using the new GWT 2.2 way to implement cell.
 * 
 * @author L.Pelov
 */
public class ImagesCell extends AbstractSafeHtmlCell<String> {
    /**
     * The HTML templates used to render the cell.
     */
    interface Templates extends SafeHtmlTemplates {
        /**
         * The template for this Cell, which includes styles and a value.
         * 
         * @param styles
         *            the styles to include in the style attribute of the div
         * @param value
         *            the safe value. Since the value type is {@link SafeHtml},
         *            it will not be escaped before including it in the
         *            template. Alternatively, you could make the value type
         *            String, in which case the value would be escaped.
         * @return a {@link SafeHtml} instance
         */
        @SafeHtmlTemplates.Template("<div name=\"{0}\" style=\"{1}\">{2}</div>")
        SafeHtml cell(String name, SafeStyles styles, SafeHtml value);
    }
 
    public ImagesCell() {
        super(SimpleSafeHtmlRenderer.getInstance(), "click", "keydown");
    }
 
    public ImagesCell(SafeHtmlRenderer<String> renderer) {
        super(renderer, "click", "keydown");
    }
 
    /**
     * Create a singleton instance of the templates used to render the cell.
     */
    private static Templates templates = GWT.create(Templates.class);
 
    private static final SafeHtml ICON_PDF = makeImage(DocIcons.RESOURCES
            .icon_doc_pdf());
    private static final SafeHtml ICON_WORD = makeImage(DocIcons.RESOURCES
            .icon_doc_word());
 
    private static final SafeHtml ICON_EXCEL = makeImage(DocIcons.RESOURCES
            .icon_doc_excel());
 
    /**
     * 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(com.google.gwt.cell.client.Cell.Context context,
            Element parent, String value, NativeEvent event,
            com.google.gwt.cell.client.ValueUpdater<String> valueUpdater) {
 
        // Let AbstractCell handle the keydown event.
        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))) {
                // if (parent.getFirstChildElement().isOrHasChild(
                // Element.as(eventTarget))) {
 
                // use this to get the selected element!!
                Element el = Element.as(eventTarget);
 
                // check if we really click on the image
                if (el.getNodeName().equalsIgnoreCase("IMG")) {
                    doAction(el.getParentElement().getAttribute("name"),
                            valueUpdater);
                }
 
            }
        }
 
    };
 
    /**
     * 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,
            String value, NativeEvent event, ValueUpdater<String> valueUpdater) {
        doAction(value, valueUpdater);
    }
 
    /**
     * Intern action
     * 
     * @param value
     *            selected value
     * @param valueUpdater
     *            value updater or the custom value update to be called
     */
    private void doAction(String value, ValueUpdater<String> valueUpdater) {
        // Trigger a value updater. In this case, the value doesn't actually
        // change, but we use a ValueUpdater to let the app know that a value
        // was clicked.
        if (valueUpdater != null)
            valueUpdater.update(value);
    }
 
    @Override
    protected void render(com.google.gwt.cell.client.Cell.Context context,
            SafeHtml data, SafeHtmlBuilder sb) {
        /*
         * Always do a null check on the value. Cell widgets can pass null to
         * cells if the underlying data contains a null, or if the data arrives
         * out of order.
         */
        if (data == null) {
            return;
        }
 
        // If the value comes from the user, we escape it to avoid XSS attacks.
        // SafeHtml safeValue = SafeHtmlUtils.fromString(data.asString());
 
        // Use the template to create the Cell's html.
        // SafeStyles styles = SafeStylesUtils.fromTrustedString(safeValue
        // .asString());
 
        // generate the image cell
        SafeStyles imgStyle = SafeStylesUtils
                .fromTrustedString("float:left;cursor:hand;cursor:pointer;");
 
        SafeHtml rendered = templates.cell("ICON_PDF", imgStyle, ICON_PDF);
        sb.append(rendered);
 
        rendered = templates.cell("ICON_WORD", imgStyle, ICON_WORD);
        sb.append(rendered);
 
        rendered = templates.cell("ICON_EXCEL", imgStyle, ICON_EXCEL);
        sb.append(rendered);
    }
 
    /**
     * Make icons available as SafeHtml
     * 
     * @param resource
     * @return
     */
    private static SafeHtml makeImage(ImageResource resource) {
        AbstractImagePrototype proto = AbstractImagePrototype.create(resource);
 
        // String html = proto.getHTML().replace("style='",
        // "style='left:0px;top:0px;"); // position:absolute;
        //
        // return SafeHtmlUtils.fromTrustedString(html);
 
        return proto.getSafeHtml();
    }
}

What is special to this class? I am using elements which already used before in my previously example, but with the latest GWT version the creation is little bit easier. First I have a template where I define my DIV which will contain the images. Then I define the images which I wanted to show. Here I am using custom creation function makeImage(ImageResource resource), which now looks very simple, since the AbstractImagePrototype has now getSafeHtml() function, so I do NOT need to convert the html code from the element to SafeHtml any more.


Another important think here is, every DIV element has a name, so that I know which image was clicked. The name can be the same for every row, because into the event I still know which row was clicked, I just want to know also which image was selected.



@SafeHtmlTemplates.Template("<div name=\"{0}\" style=\"{1}\">{2}</div>")
SafeHtml cell(String name, SafeStyles styles, SafeHtml value);

Now using this we can render the cell to show our images, here only the snippet from the code above:



@Override
protected void render(com.google.gwt.cell.client.Cell.Context context,
        SafeHtml data, SafeHtmlBuilder sb) {
    /*
     * Always do a null check on the value. Cell widgets can pass null to
     * cells if the underlying data contains a null, or if the data arrives
     * out of order.
     */
    if (data == null) {
        return;
    }
 
 
    // generate the image cell
    SafeStyles imgStyle = SafeStylesUtils
            .fromTrustedString("float:left;cursor:hand;cursor:pointer;");
 
    SafeHtml rendered = templates.cell("ICON_PDF", imgStyle, ICON_PDF);
    sb.append(rendered);
 
    rendered = templates.cell("ICON_WORD", imgStyle, ICON_WORD);
    sb.append(rendered);
 
    rendered = templates.cell("ICON_EXCEL", imgStyle, ICON_EXCEL);
    sb.append(rendered);
}

As you can see you can call the template.cell more then once and then put everything to the SafeHtmlBuilder to render. Also I used here SafeStylesUtils to generate my CSS style for the DIV’s so that the images will be shown in one line. You can of course go for CSS style directly if you wish.


How to handle the events? This is the interesting part now. Take a look at the onBrowserEvent function.



@Override
public void onBrowserEvent(com.google.gwt.cell.client.Cell.Context context,
        Element parent, String value, NativeEvent event,
        com.google.gwt.cell.client.ValueUpdater<String> valueUpdater) {
 
    // Let AbstractCell handle the keydown event.
    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);
 
            // check if we really click on the image
            if (el.getNodeName().equalsIgnoreCase("IMG")) {
                doAction(el.getParentElement().getAttribute("name"),
                        valueUpdater);
            }
 
        }
    }
 
};

What I am doing here, I am trying to make sure that the image was clicked not the DIV. Also I remove the code checking for first child since my IMG does not have any child. So if IMG into the cell was clicked then I pass the event to the doAction function. Also I am checking the parent element of the IMG since there is the DIV with the name predefined so I know now which image was clicked. Inside the doAction method I just call the ValueUpdater and pass the name of the DIV which was containing the clicked picture, that’s it.


To be able to use this cell into CellTable you have to subclass it to column like this:



public abstract class ImagesColumn<T> extends Column<T, String> {
    public ImagesColumn() {
        super(new ImagesCell());
    }
}

Now this column is something you can use into any CellTable, for example like this:



ImagesColumn<Contact> imagesColumn = new ImagesColumn<Contact>(){
    @Override
    public String getValue(Contact object) {                
        return object.color;
    }
};

Ok so one think left, as you remember we implement some custom code into the onBrowserEvent so we know which image is clicked into the cell, so how to use this here?


The solution: ValueUpdater calls the FieldUpdater, if you subclass cell to column you will have the value passed to the ValueUpdater inside the FieldUpdater, if you go for this:



// try to use the filed updater to call the value updater!!!
imagesColumn.setFieldUpdater(new FieldUpdater<CellTableSortingView23Impl.Contact, String>() {
    
    @Override
    public void update(int index, Contact object, String value) {
        
        Window.alert("This is the field updater, with value: " + value + " Index: " + index + " Contact: " + object.name);
        
    }
});

the value passed to the update() function is the value you passed into the onBrowserEvent to the ValueUpdater.


If you click now on one of the images you will get this:


 


image


As you can see you still have the index value and the object, but you also know which image was clicked.


You can find the examples HERE.


Direct Links:


ImageCell.java


ImagesColumn.java


CellTableSortingView23Impl.java

6 comments:

  1. your GWT articles have been very helpful for me. thanks!

    ReplyDelete
  2. Exactly what I needed to implement. Nice clean code, thanks for sharing.

    ReplyDelete
  3. Thanks a lot for the article. Very helpful.I dont have your id to add. Do stay in touch!

    ReplyDelete
  4. thanks for sharing! very helpful. :)

    ReplyDelete
  5. Thank you very much. Especially explaining how to find on which element the click event occurred has helped me solve my problem with the CellTable.

    ReplyDelete