Widgets in GWT Cell Tables

I have recently had the need to embed widgets into a GWT cell table.  I have read about the flyweight pattern, and specifically why cell tables are designed to not have widgets in them, but… when you have a requirement to complete you just need to do it.

I went through two iterations of this design and I am pretty happy with the final design.  I took some ideas from GWT Cell Widgets like EditTextCell as well as some experience from the first not-so-great implementation i created.  If you are new to Cells, it is important to realize that the Cell is instantiated one time, but called (with different data) for each row that is rendered.  This is why it is necessary to maintain a map of this cells data.

Here are some design ideas for this “WidgetCell”

  • Widget creation is expensive, so a widget for each row is only created one time and then stored in a cache for reuse.  This helps with the speed of the table rendering.
  • Widgets are never attached to the DOM.  They exist to be manipulated in memory and then have their HTML extracted and pushed into the DOM.
  • Events from the Widgets, therefore, are not handled.  If you need them you will need to change this implementation a little and pass them through the cell (onBrowserEvent method).  Cell events ARE handled (or can be in subclasses), which was enough for my purposes.
  • Finally, the widgets that I am using need to load data from the backend, so this “WidgetCell” populates the cells asynchronously.  This helps speed up initial table rendering as well.

One important thing to note on that last bullet: Browsers have a limit on the number of HTTP requests that can be active at a given time.  Additional requests (from my experience, i didn’t look into it deeply) are getting queued and block the UI from any other HTTP requests until they are cleared from the queue.  This means anything, like navigation, that requires an HTTP request will be queued and block the user interface.  The best solution I have found for this is to pass all related HTTP requests, like the ones for this widget, through a common RPC handler that performs queuing.  This can limit that data providers allowable HTTP requests, leaves additional HTTP connections available, and allows for cancelling uncalled or in-process requests if navigation changes prior to the table completely populating.

This WidgetCell is an abstract class.  It is meant to be extended, with the implementation defining the widget and handling the widget’s data.  The data can be stored in a map with an entry per row.  The abstract methods all pass through either the CellContext, the Cell context’s key, or the Cell index, depending on that is needed for that method.

Important things to remember:

  1.  As I mentioned earlier, this is a method for displaying widgets into Cells.  The widget lifecycle is NOT respected.  There is no attaching and detaching, and events are not handled from the widget.
  2. Widgets that change themselves over their lifetime will NOT have those changes reflected in the cell unless you take special care to manually reflect those changes in the DOM.  An example is a progress bar.  It will be rendered to the DOM, but any updates need to be re-rendered to the DOM manually because the widget is not attached.

Here is the code.  I am sure there will be questions that i can handle in the comments.  Additionally, I need to thank Colin Alworth at Sencha for comments and pointing out pitfalls that helped direct this final design, and David Maddison’s The GWT rendering process post for more insight on the widget lifecycle.

Here is a file with the code: Textfile with WidgetCell Code


import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import com.google.gwt.cell.client.AbstractCell;
import com.google.gwt.cell.client.EditTextCell;
import com.google.gwt.cell.client.ValueUpdater;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
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.user.client.Command;
import com.google.gwt.user.client.ui.RequiresResize;
import com.google.gwt.user.client.ui.Widget;

/**
 * A Cell for adding Widgets into CellTables. A single instance of a cell is used for a column, with the render method
 * being called for each new cell in a new row. data changes per row must come in through the render method (via the
 * setValue method in super)
 * 
 * The approached used here:
 * <ol>
 * <li>Keep a cache of widgets so only a finite number are made, and they are reused to minimize expensive widget
 * creation (this is based on the method GWT's {@link EditTextCell} uses to maintain data.)</li>
 * <li>The widgets are never attached to the DOM, they are only used for manipulating them and extracting their HTML</li>
 * <li>Events from the widgets, therefore, are not handled. Only events on the cells themselves. However, if needed, the
 * cell events can be passed along to the widgets in the reuse-map and handled. (the context contains the key for the
 * reuse-map)</li>
 * <li>For ajax use, only a wrapper (placeholder) is laid down in the DOM. This placeholder is replaced when the widget
 * HTML when it is ready.</li>
 * </ol>
 * 
 * @author mpickell
 * @version 1.0
 * @param <W>
 *            the widget that is to be used in this cell
 * @param <D>
 *            the type of data used for this widget. This is passed in through the render method for each cell created.
 * 
 * @see <b>For more information on this implementation:</b>
 *      http://www.sencha.com/forum/showthread.php?196112-Widget-Rendered-Grid/page2
 * 
 *      TODO: rename to AbstractClickableWidgetCell?
 * 
 */
public abstract class ClickableWidgetCell<W extends Widget, D> extends AbstractCell<D> implements RequiresResize {

    /**
     * Widget generator for use by this cell renderer. This used as a factory that is used when a new widget is needed.
     * 
     * @param <C>
     *            widget type returned
     */
    protected interface ClickableWidgetCellWidgetFactory<C extends Widget> {
        C createWidget();
    }

    private static int uniqueKeyForWidgetID = 0;

    /**
     * Widget's displaying the table may enlarge it. If this is non-null, it will be resized when all widgets display.
     */
    private RequiresResize containerToResizeAfterAllWidgetsDisplay;

    /**
     * map of widgets used in this column. Once created, the widget is cached so it doesn't need to be created again.
     * The key here is the context.getKey() object, which uniquely IDs a row. (reuse-map is based on design of GWT's
     * {@link EditTextCell})
     */
    private Map<Object, W> reusableWidgetsMap = new HashMap<Object, W>();
    private Map<Object, String> domPlaceholderIds = new HashMap<Object, String>();

    /**
     * This cell can be used as a non-clickable cell as well. this boolean will block the click and keydown events when
     * set to true via the setter.
     */
    private boolean blockClickableEvents = false;

    /**
     * The HTML templates used to render the cell placeholder.
     */
    interface ClickableWidgetCellPlaceholderTemplate extends SafeHtmlTemplates {
        /**
         * The template for the widget placeholder for this Cell, which includes styles and a value.
         * 
         * @param id
         *            the id for this placeholder template
         * @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 of a simple container. The container position is set to relative so that
         *         any value placed inside can use absolute positioning.
         */
        @SafeHtmlTemplates.Template("<div style='position:relative;' id='{0}'>{1}</div>")
        SafeHtml cell(String id, SafeHtml value);
    }

    /**
     * Create a singleton instance of the templates used to render the cell.
     */
    private static ClickableWidgetCellPlaceholderTemplate placeholderTemplate = GWT
            .create(ClickableWidgetCellPlaceholderTemplate.class);

    /**
     * Construct a new ClickableWidgetCell
     */
    public ClickableWidgetCell() {
        super("click", "keydown");
    }

    /**
     * @return a factory that creates the default widget type, with any necessary setup, that is used by this Dto.
     */
    protected abstract ClickableWidgetCellWidgetFactory<W> getWidgetFactory();

    /**
     * Trigger any asynchronous calls that are needed to get data, if needed, and then update the widget. When update is
     * complete, execute command to push widget HTML to DOM.
     * 
     * @param widget
     *            the widget that needs to be updated
     * @param onDataReadyCallback
     *            the command to trigger when the widget is updated. This command displays the widget in the page
     */
    protected abstract void updateWidget(final W widget, final Context cellContext, final D data,
            final Command onDataReadyCallback);

    /**
     * After the widget is displayed, handle any clean up (like resize to fill cell maybe). This is a default impl.
     * 
     * @param widget
     *            the widget that was displayed in the DOM
     * @param cellContext
     *            this widget's context
     * @param data
     *            the data used when displaying this widget.
     */
    protected void finalizeWidgetAfterDisplayInGrid(final W widget, final Context cellContext, final D data) {
    }

    /**
     * When resize is triggered on this cell (passed down from the grid) the cell implementation can handle it. This is
     * the default impl.
     * 
     * @param widget
     *            a widget from the reuse queue
     * @param cellContextKey
     *            this widget's context
     * @param placeholderId
     *            the placeholder ID for this widget in the DOM, so the HTML can be updated.
     */
    protected void handleWidgetOnWindowResize(W widget, Object cellContextKey, String placeholderId) {
    }

    /**
     * Default implementation.
     * 
     * This method returns HTML to be placed in the page while waiting for the widget to be displayed. If the widget
     * requires service calls, this will be displayed until the service calls return and the onDataReadyCallback command
     * is executed.
     * 
     * @return a SafeHtml object containing whatever HTML should be displayed while the main widget is being updated.
     *         This will be completely removed and placed by the widget HTML when the updateWidget method calls the
     *         onDataReadyCallback.
     */
    protected SafeHtml getWidgetUpdatingHtml() {
        return new SafeHtml() {
            private static final long serialVersionUID = 1L;

            @Override
            public String asString() {
                return "<i>Loading...</i>";
            }
        };
    }

    @Override
    public void onBrowserEvent(Context context, Element parent, D widgetDataDto, NativeEvent event,
            ValueUpdater<D> valueUpdater) {
        super.onBrowserEvent(context, parent, widgetDataDto, event, valueUpdater); // Handles user press actual
                                                                                   // ENTER key
        if ("click".equals(event.getType())) {
            onEnterKeyDown(context, parent, widgetDataDto, event, valueUpdater);
        }
    }

    @Override
    protected void onEnterKeyDown(Context context, Element parent, D widgetDataDto, NativeEvent event,
            ValueUpdater<D> valueUpdater) {
        if (!blockClickableEvents && valueUpdater != null) {
            valueUpdater.update(widgetDataDto);
        }
    }

    @Override
    public void render(final Context context, final D widgetDataDto, final SafeHtmlBuilder sb) {
        /*
         * Widgets are reused so that their (expensive) creation is not performed each time. After creation, they are
         * attached and detached as needed and re-added to the page.
         */
        Object rowKey = context.getKey();

        /*
         * Either get the existing widget from the cache or create it.
         */
        W wfc = reusableWidgetsMap.get(rowKey);
        if (wfc == null) {
            // Create once and store.
            wfc = this.getWidgetFactory().createWidget();
            reusableWidgetsMap.put(rowKey, wfc);
        }
        final W wfcAsFinal = wfc;

        /*
         * Setup a unique key for the placeholder in this row
         */
        if (!domPlaceholderIds.containsKey(context.getKey())) {
            setupPlaceholderId(context);
        }

        /*
         * Allow the Dto to update the widget, possibly after asynchronous calls to service. Once updated, the command
         * is (i.e., has to be) executed to finalize the displaying of the cell.
         * 
         * This update is triggered as a GWT "thread" using the deferred command so that the render method can complete
         * quickly. The update and display of the widget can be deferred safely because everything needed for it is
         * packaged in the updateWidget method
         */
        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
            @Override
            public void execute() {
                updateWidget(wfcAsFinal, context, widgetDataDto, new Command() {
                    @Override
                    public void execute() {
                        ClickableWidgetCell.this.displayWidget(context, widgetDataDto);
                    }
                });
            }
        });

        /*
         * Put the placeholder in the table so we have a location to go to later. Put the "busy" html in there.
         */
        sb.append(placeholderTemplate.cell(domPlaceholderIds.get(context.getKey()), getWidgetUpdatingHtml()));
    }

    private void displayWidget(final Context cellContext, final D widgetDataDto) {
        Element e = getElementsBySelectorJSNI("#" + domPlaceholderIds.get(cellContext.getKey()));
        if (e != null && reusableWidgetsMap.containsKey(cellContext.getKey())
                && reusableWidgetsMap.get(cellContext.getKey()).getElement() != null) {
            e.setInnerHTML(reusableWidgetsMap.get(cellContext.getKey()).getElement().getString());

            /*
             * The widget is now displayed. Allow the implementation to perform any adjustments to it. The
             * implementation can use the domPlaceholderIds map (via getter) to replace the HTML in the page since the
             * element in the widget is never attached to the page.
             * 
             * Changes to the widget will be reflected in the widget map, placeholder ID can be used (via getter, as a
             * string so it cannot be changed) to push any updated HTML to the page.
             */
            Scheduler.get().scheduleDeferred(new ScheduledCommand() {
                @Override
                public void execute() {
                    ClickableWidgetCell.this.finalizeWidgetAfterDisplayInGrid(reusableWidgetsMap.get(cellContext.getKey()),
                            cellContext, widgetDataDto);
                }
            });

            /*
             * This is a widget that might be expanding the table. Call on the resize container to resize.
             */
            if (containerToResizeAfterAllWidgetsDisplay != null) {
                containerToResizeAfterAllWidgetsDisplay.onResize();
            }
        }

    }

    /**
     * Query for elements using selectors
     * 
     * @param selectors
     *            a string containing one or more CSS selectors separated by commas
     * @return a JS array of elements.
     * 
     * @see https://developer.mozilla.org/en-US/docs/DOM/Document.querySelector
     * @see http://www.w3.org/TR/selectors-api/
     */
    protected native Element getElementsBySelectorJSNI(String selector) /*-{
		return $doc.querySelector(selector);
    }-*/;

    /**
     * Create a unique ID for a placeholder that will be placed in the cell on render.
     * 
     * @param context
     */
    private void setupPlaceholderId(Context context) {
        if (uniqueKeyForWidgetID >= Integer.MAX_VALUE) {
            // housekeeping.
            uniqueKeyForWidgetID = 0;
        } else {
            uniqueKeyForWidgetID++;
        }
        domPlaceholderIds.put(context.getKey(), "WidgetCellID_" + uniqueKeyForWidgetID);
    }

    /**
     * If true, this will block the clickable events and this cell will simply render the widget without accepting
     * events.
     * 
     * @param blockClickableEvents
     */
    public void setBlockClickableEvents(boolean blockClickableEvents) {
        this.blockClickableEvents = blockClickableEvents;
    }

    @Override
    public void onResize() {
        /*
         * on resize of the page, update the widget to the new cell size if necessary.
         */
        if (reusableWidgetsMap != null && !reusableWidgetsMap.isEmpty()) {
            for (Entry<Object, W> entry : reusableWidgetsMap.entrySet()) {
                ClickableWidgetCell.this.handleWidgetOnWindowResize(entry.getValue(), entry.getKey(),
                        domPlaceholderIds.get(entry.getKey()));
            }
        }
    }

    /**
     * Get the ID for the widget placeholder in the page. the innerHtml of this element is the widget HTML.
     * 
     * @param cellContextKey
     * @return
     */
    protected String getDomPlaceholderId(Object cellContextKey) {
        return domPlaceholderIds.get(cellContextKey);
    }

    /**
     * Set the container of this grid so it can be resized when all widgets display.
     * 
     * @param containerToResizeAfterAllWidgetsDisplay
     *            the container that implements RequiresResize & IsWidget
     */
    public void setContainerToResizeAfterAllWidgetsDisplay(RequiresResize containerToResizeAfterAllWidgetsDisplay) {
        this.containerToResizeAfterAllWidgetsDisplay = containerToResizeAfterAllWidgetsDisplay;
    }

}

Share

13 thoughts on “Widgets in GWT Cell Tables”

  1. Have you tried compiling this? Looks like the generics is a bit off as im getting errors with IntelliJ, Can you show an example?

  2. ok, i have added a text file with the code to avoid any paste issues with the visible code.

    Please try the code from that text file and let me know if you still have an issue. This code is cut directly from a compiling, running app so I know it is working.

  3. Not sure how to use this yet, just tried it in a grid and dont see the label show up:

    public class VNWidgetCell extends ClickableWidgetCell {

    @Override
    protected ClickableWidgetCellWidgetFactory getWidgetFactory() {
    return new ClickableWidgetCellWidgetFactory() {
    @Override
    public Widget createWidget() {

    Label myLabel = new Label();
    myLabel.setText(“HEY!!!!”);
    myLabel.setWidth(“100”);
    myLabel.setHeight(“100”);

    return myLabel; //To change body of implemented methods use File | Settings | File Templates.
    }
    };
    }

    @Override
    protected void updateWidget(Widget widget, Context cellContext, Object data, Command onDataReadyCallback) {

    System.out.println(“updateWidget () called.” + widget);
    }

    protected SafeHtml getWidgetUpdatingHtml() {
    return new SafeHtml() {
    private static final long serialVersionUID = 1L;

    @Override
    public String asString() {
    return “”;
    }
    };
    }
    }

  4. Check the comments on the updateWidget method.

    That method is meant to support ajax… So after you do whatever you need to do to manipulate the widget, you need to execute the command.

    So after your System.out.println call, call onDataReadyCallback.run()

  5. Can you please provide an example on how one can pass cell events to the widget.
    I have a Tree widget and FlexTable are not refreshing. The html gets generated and passed to the cell but that’s it, just the html without the actual widgets working. I know you mentioned this:
    If you need them you will need to change this implementation a little and pass them through the cell (onBrowserEvent method). Cell events ARE handled (or can be in subclasses), which was enough for my purposes.

    thank you !

    1. Sorry I didn’t get back to you sooner.. I have not worked on GWT for about a year so I had to review this post.

      Let me know if this helps out:

      This class has an onBrowserEvent method which handles events that happen to the cell. You can see that I override and extend this method to handle “click” as well. You can override it and add an additional event type, add a new method like the “onEnterKeyDown” method, and perform whatever action you want on the widget.

      So, there are a couple options for handling the events:

      If you actually need to affect the widget, you would want to expose the “reusableWidgetMap” (make it protected) so you can access the widget that is in the cell where the event occurred. The key of the reusableWidgetMap is passed into the onBrowserEvent method and is context.getKey().
      ** You need to consider here that the widgets are reusable, so changes that you make will be reused again for that row with different data.
      ** Also, if you change the widget, you will need to push its HTML back out to the page after manipulating it.
      ** Finally, depending on the change you need, consider changing the widget used by the factory and all other widgets. Or maybe you can make the change in the widgetDataDto object and program the widget to look into that data for any customizations you want.

      If you only need to affect the visual representation of the widget in the page, then use the getDomPlaceholderId(context.getKey()) to get the ID of the element in the DOM and manipulate that element directly based on the event triggered on the cell.

      If you need to just trigger some event in the system and not change the widget, then you can capture the event this same way and just use it to trigger whatever you want in the system. You get the “widgetDataDto” so you should have anything that you need from the cell.

      Finally, you can register events on the HTML elements themselves and handle the events. In your implementation of this ClickableCellWidget class, override “finalizeWidgetAfterDisplayInGrid.” In that method, you can use the getDomPlaceholderId(context.getKey()) method to get the element, and then register events directly into the page.

      Those are a couple of methods of capturing the event from the cell and using it in some fashion, depending on your need.

  6. For example, my tree widget looks something like this, instead of the working Tree. I am assuming that the widget is not even being rendered in my gxt grid cell and only the html is being copied into place ?

    p1
    c1
    c2
    c3

    1. yes, only the HTML is being pulled out of the widget and pushed to the page. Cells specifically handle only HTML and not Widgets in order to stay lightweight (and that’s why a class like ClickableCellWidget is not delivered with GWT).

      Are you using an older version of GXT? If I recall, older versions would put placeholders into the page and render to the placeholder. The newer version that I last worked with (v3?) handled widgets more like GWT, where the widget was fully rendered when placed in the page.

      If you are using an older version of GXT, you might have to take additional steps on your widget, like calling “onAttach()” manually in order to force it to render after this ClickableCellWidget places the HTML in the page. You could probably do that in the “finalizeWidgetAfterDisplayInGrid” method.

  7. Thanks so much for the reply and help. I am just not making it happen. I am using GXT 3.0.1 GA release.

    public class TreeWidgetCell extends ClickableWidgetCell {

    @Override
    protected ClickableWidgetCellWidgetFactory getWidgetFactory() {
    return new ClickableWidgetCellWidgetFactory() {
    @Override
    public Widget createWidget() {

    TreeItem node1 = new TreeItem();
    node1.setText(“p1”);
    node1.addTextItem(“c1”);
    node1.addTextItem(“c2”);
    node1.addTextItem(“c3”);

    TreeItem node2 = new TreeItem();
    node2.setText(“p2”);
    node2.addTextItem(“c1”);

    Tree tree = new Tree();
    tree.addItem(node1);
    tree.addItem(node2);

    tree.setAnimationEnabled(true);
    tree.ensureDebugId(“cwTree-staticTree”);

    return tree;
    }
    };
    }

    @Override
    protected void updateWidget(Widget widget, Context cellContext, Object data, Command onDataReadyCallback) {
    onDataReadyCallback.execute();
    }

    @Override
    protected SafeHtml getWidgetUpdatingHtml() {
    return super.getWidgetUpdatingHtml();
    }

    @Override
    protected void finalizeWidgetAfterDisplayInGrid(Widget widget, Context cellContext, Object data) {

    }
    }

    I still just get the following html rendered to browser without the widget drawing/rendering/firing the javascript ?? I believe it’s more related to attaching itself to the DOM ? Could you throw some code up on how render a widget in the cell ?

    Thanks

    p1
    c1
    c2
    c3
    p2 c1

    1. Hmm.. I’m not sure. You are setting up the code correctly. It may be something specific to the Tree… It is a complex widget.

      Have you tried testing different things like calling tree.onAttach(), or something like the ideas in this post (although this is probably an older version of GXT): http://stackoverflow.com/questions/4786959/how-to-force-a-redraw-re-layout-in-ext-gwt-gxt

      Try to call onAttach() in the updateWidget method before executing the command. See if the tree has any “forceLayout” methods or some method like that.

      Take a look here for a widget lifecycle: http://stackoverflow.com/questions/8959643/how-to-know-when-a-widget-is-being-rendered

      i’ll take a closer look and see if I can anything else.

    2. Please try to call onAttach() before calling “onDataReadyCallback.execute();” in the updateWidget method.

      Taking a look at the code for the tree, it overrides the onAttach() method and adds a call to “update”. Depending on how you have it set up, you may instead try calling doUpdate() on the tree.

      Let me know if that gives you better results.

    1. Example of what? A basic usage example? Or an example using a GXT widget?

      I don’t have a GWT system actually running anymore. I can put something together, but let me know exactly what kind of example you would like.

      I can’t guarantee that this would work with the TreeWidget that mantis was asking about. He did not get back to me with an answer as to whether or not he got it going.

      Just to make sure:
      At the end of the updateWidget method, you are calling “onDataReadyCallback.run()”, correct? You can set everything else up correctly, but if you don’t call that method it will never push the widget to the page. It is meant so that you can perform some ajax data action prior to pushing the widget into the page… but you have to call it or it won’t work, even if you just call it immediately in the updateWidget method.

Leave a Reply

Your email address will not be published. Required fields are marked *