Friday, May 27, 2011

Making Links Work Right in a SmartGWT App on IE

The GUI of RHQ 4.0 and later is built upon SmartGWT. In many places in the SmartGWT app, we embedded raw HTML to render fragment links (e.g. #Inventory/Servers) to other places in the app. We used raw HTML, rather than widgets, for a few reasons:

  1. SmartGWT does not provide a link widget. GWT provides the HyperLink and InlineHyperLink widgets, but we try to avoid using non-SmartGWT widgets when possible to prevent layout issues or CSS issues caused by straying from the SmartGWT framework. A SmartGWT Label or HTMLFlow can be extended to simulate a link using a ClickHandler but it will not be rendered as an 'a' tag and so will not inherit the CSS styles used for 'a' tags and will not display the link's URL in the browser status bar when the user hovers over the link.
  2. Many of our links are inside ListGrid cells. There is no straightforward reliable way to embed arbitrary widgets in ListGrid cells. I tried using the mechanism http://www.smartclient.com/smartgwt/showcase/#grid_cell_widgets described here and encountered overflow and wrapping issues, which I was unable to overcome. The CellFormatter interface only supports returning a String, but that String can include HTML, so that's what we ended up using for cells that need to contain a link.
  3. For FormItems that need to contain links, CanvasItem can be extended in order to embed GWT HyperLink widgets, but using a StaticTextItem with HTML embedded in its value is more straightforward.
Unfortunately, we noticed that clicking on any of our raw HTML fragment links in IE caused a full page refresh, which is not at all desirable in a GWT app, which is intended to be pure AJAX. Further investigation revealed that this is a longstanding quirk (aka bug) in IE; rather than simply generating a history event for the URL with the updated fragment, it sends an unnecessary request for the URL to the server. If you use the GWT HyperLink widget, GWT uses some JavaScript fanciness involving iframes to circumvent the IE bug and make fragment links work properly. However, since we were using raw HTML for all the links in the RHQ GUI, this magic was not there for us. Converting all our HTML links would be a ton of work and was simply not a viable option for links in ListGrid cells for the reasons described above, so we needed to find a way to execute GWT's magic when any of our raw HTML links were clicked. The answer ended up being to add a native preview event handler that intercepts browser click events and executes the magic if the click was on one of our 'a' tags. We did this by making our EntryPoint class implement the GWT Event.NativePreviewHandler interface as follows:
    public void onPreviewNativeEvent(Event.NativePreviewEvent event) {
        if (SC.isIE() && event.getTypeInt() == Event.ONCLICK) {
            NativeEvent nativeEvent = event.getNativeEvent();
            EventTarget target = nativeEvent.getEventTarget();
            if (Element.is(target)) {
                Element element = Element.as(target);
                if ("a".equalsIgnoreCase(element.getTagName())) {
                    // make sure it's not a hyperlink that GWT already
                    // handles
                    if (element.getPropertyString("__listener") == null) {
                        String url = element.getAttribute("href");
                        String historyToken = getHistoryToken(url);
                        if (historyToken != null) {
                            GWT.log("Forcing History.newItem(\"" +
                                historyToken + "\")...");
                            History.newItem(historyToken);
                            nativeEvent.preventDefault();
                        }
                    }
                }
            }
        }
    }

    private static String getHistoryToken(String url) {
        String token;
        if (url.startsWith("#")) {
            token = url.substring(1);
        } else if (url.startsWith("/#")) {
            token = url.substring(2);
        } else if (url.contains(Location.getHost()) && url.indexOf('#') > 0) {
            token = url.substring(url.indexOf('#') + 1);
        } else {
            token = null;
        }
        return token;
    }
We then add the native preview handler at app load time by adding the following line to our EntryPoint class:
Event.addNativePreviewHandler(this);
This solution is working great. However, we still might eventually go back and switch over to using GWT HyperLinks, rather than raw HTML, in places where it is feasible, such as FormItems, since it is generally better to use widgets rather than raw HTML to keep things object-oriented and leave the generation of HTML, CSS, and JavaScript to the framework.

1 comment:

daz said...

Have you considered using LinkItem in the SmartGWT framework?

com.smartgwt.client.widgets.form.fields.LinkItem