Thursday, July 24, 2008

Cross Domain FormPanel submissions in GWT

While working on Timelord, our next generation charting and analytics tool, I found the need to do cross-domain REST requests because Timelord may be hosted anywhere, but the server-side services are running on a fixed domain (hosted by Google App Engine). Recently, abuse cool hacks using the window.name field have become en vogue, much like the earlier activity around fragment identifiers, and one particularly cool usage is the recent Dojo proposal to use window.name for cross-domain cross-frame communication.

After a little experimenting, and some snooping on the Dojo patch, I implemented a quick patch to GWT using Deferred Binding, here's an example:


public class WindowNameFormPanelImpl extends FormPanelImpl {

/**
* Gets the response html from the loaded iframe's name property
*
* @param iframe the iframe from which the response html is to be extracted
* @return the response html
*/
public native String getContents(Element iframe) /*-{
try {
// Make sure the iframe's window & document are loaded.
if (!iframe.contentWindow || !iframe.contentWindow.document)
return null;

// Get the response from window.name
return iframe.contentWindow.name;
} catch (e) {
return null;
}
}-*/;

/**
* Hooks the iframe's onLoad event and the form's onSubmit event.
*
* @param iframe the iframe whose onLoad event is to be hooked
* @param form the form whose onSubmit event is to be hooked
* @param listener the listener to receive notification
*/
public native void hookEvents(Element iframe, Element form,
FormPanelImplHost listener) /*-{
if (iframe) {
iframe.onload = function() {
// If there is no __formAction yet, this is a spurious onload
// generated when the iframe is first added to the DOM.
if (!iframe.__formAction)
return;

if(!iframe.__restoreSameDomain) {
iframe.__restoreSameDomain = true;
// restore same domain property of iframe to read window.name property
iframe.contentWindow.location =
@com.google.gwt.core.client.GWT::getModuleBaseURL()() +
"clear.cache.gif";
return;
}
listener.@com.google.gwt.user.client.ui.impl.FormPanelImplHost::onFrameLoad()();
};
}

form.onsubmit = function() {
// Hang on to the form's action url, needed in the
// onload/onreadystatechange handler.
if (iframe) {
iframe.__formAction = form.action;
iframe.__restoreSameDomain = false;
}
return listener.@com.google.gwt.user.client.ui.impl.FormPanelImplHost::onFormSubmit()();
};
}-*/;
}


And in your GWT module

<replace-with class="org.timepedia.timelord.client.impl.WindowNameFormPanelImpl">
<when-type-is class="com.google.gwt.user.client.ui.impl.FormPanelImpl"/>
</replace-with>


With this code, you simply submit a FormPanel to a service with an additional parameter, like "windowname=true" which indicates you want window.name transport. The resulting output of a POST should contain something like "<script>window.name = '....result string...'</script>. When the FormPanel.submit() method is invoked, this result will be passed to the FormHandler.onSubmitComplete(event), and you can retrieve the result via event.getResult().

-Ray
p.s. there are separate implementations for IE

Friday, July 4, 2008

Chronoscope GViz API and Gadget released

Hey guys, a beta of Chronoscope 0.86 (in the trunk) has been packaged up as a GViz API and Google Gadget (with Spreadsheets integration). Check it out at Timepedia.org to see the new features.

-Ray