Hi folks,
I had to delay the second part of my article on Linkers because the APIs have been changing radically lately and have only just settled down in the trunk. However, to pass the time, I decided to demonstrate some of the huge benefits of 1.5 by implementing a type-safe JQuery clone in GWT.
That's right, for all those folks who don't want to deal with GWT Widgets, but just want to query, wrap, and manipulate plain old DOM elements in GWT, this article is for you. I'm talking to you, you self-hating Java programmer with an inner Javascript Ninja begging to be let out.
Yes this is working code
First, let's just dispense with the details and show you a working example, yes, this is working code:
public void onModuleLoad() {
$("div").css("color", "red").click(new Function() {
public void f(Element e) {
Window.alert("Hello");
$(e).as(Effects).fadeOut();
}
});
}
What does this do? It makes every DIV element red and clickable, and if you click one, it pops up an alert message, and fades the DIV out.
The API is mostly faithful to jQuery's public API, with the exception of the lack of succinct closures (Hello, Neal Gafter!), but that's ok, we can introduce enough prebuilt composable functors to eliminate alot of the need for inline anonymous functions for common operations.
Small, Fast, Efficient Code, and Other Benefits
Since GWT prunes any code not being used, this example produced a obfuscated + gzipped script size of only 5459. If I change the click handler to simply $(e).setInnerHTML("Boom"), the compiled + gzipped script size is 1400 bytes.
There's a few small differences to take note which are either a benefit, or a hinderance depending on who you ask:
1. Type-Safety, IDE code completion, debuggability, etc
2. Lack of prototype modifications forced slighty change for GQuery plugin usage (see Effects above)
3. Selectors can be compiled at compile time, more on that later.
How 1.5 Enables GQuery
Now, let's talk about the features in GWT 1.5 that make GQuery nice and efficient to implement.
1) Joel Webber of the GWT Team just submitted a patch to GWT 1.5 that implements every DOM2 Core+HTML class as a subclass of the new JavaScriptObject. This not only made the coding of GQuery very easy, but it produces nicely optimized JavaScript.
2) Static Imports. The $ function would not nearly be as nice without static imports.
3) Generics and Covariant return. The Plugin mechanism utilizes this to fake prototype based overrides of the GQuery object.
How to write GQuery plugins? Damn simple
Java doesn't feature metaclasses or prototypes, so there is no way to add methods to an existing instance or class without subclassing, but in order to preserve jQuery style syntax, we can use covariant return, type capture, and other features of JDK 1.5, to fake it.
There are two ways this occurs, first, the $() method can take a final parameter which is a Plugin class literal, or, you can call the as() method on any GQuery and "cast" it to a plugin interface. Here's the implementation of the as() method:
/**
* Convert to Plugin interface provided by Class literal.
*/
public <T extends GQuery> T as(Class<T> plugin) {
return (T) plugins.get(plugin).init(this);
}
The type of the plugin, which is a subclass of GQuery is captured by the class literal parameter, the Class literal is mapped to a Plugin class factory which creates instances of the plugin, and finally, the current GQuery is passed in, so that the Plugin can access the set of matched elements.
Here is an example plugin implementation:
public class Effects extends GQuery {
static {
GQuery.registerPlugin(Effects.class, new EffectsPlugin());
}
public static final Class<Effects> Effects = Effects.class;
public Effects(Element element) {
super(element);
}
public Effects(Element[] elements) {
super(elements);
}
public Effects(NodeList list) {
super(list);
}
public Effects fadeOut() {
Animation a = new Animation() {
public void onCancel() {
}
public void onComplete() {
for(Element e : elements) {
e.getStyle().setProperty("opacity", "1.0");
}
}
public void onStart() {
}
public void onUpdate(double progress) {
for(Element e : elements) {
e.getStyle().setProperty("opacity", String.valueOf(1.0-progress));
}
}
};
a.run(2000);
return this;
}
public Effects fadeIn() {
Animation a = new Animation() {
public void onCancel() {
}
public void onComplete() {
}
public void onStart() {
}
public void onUpdate(double progress) {
for(Element e : elements) {
e.getStyle().setProperty("opacity", String.valueOf(progress));
}
}
};
a.run(2000);
return this;
}
public static class EffectsPlugin implements Plugin<Effects> {
public Effects init(GQuery gq) {
return new Effects(gq.get());
}
}
}
That's it. Plugins are nothing more than subclasses of GQuery which have an extra registry step and factory.
I'll put up the source code on http://code.google.com/p/gwtquery after GWT 1.5 milestone 2 is released, which is needed to run this.
You forgot to talk about compile-time selectors
So, yeah, as Generators are a frequent topic of this blog, you really think I wouldn't find a way to sneak them in? GQuery supports two mechanisms for selector evaluation. It can compile and evaluate selectors at runtime (currently just using XPath for prototyping, which isn't available everywhere), or, you can create an interface containing selectors you know about at runtime, and have them precompiled and ready to execute.
As an example:
public interface MySelectors extends GQuery.Selectors {
@Selector("div")
public GQuery allDivs();
@Selector("div.foo")
public GQuery allFooDivsBelow(Element context);
}
Then to use:
MySelectors s = (MySelectors)GWT.create(MySelectors.class);
s.allFooDivsBelow(context).css("backgroundColor", "yellow");
A generator performs a browser-specific compile-time transformation of the selector in the annotation into the most optimal form that the browser supports (XPath, pure-JS, DOM traversal, document.getElementsByClass, etc) Not only does this produce smaller per-browser code, it produces faster code as well.
One last thing, the $$ function
jQuery contains a lot of methods which take essentially property/value pairs in a Javascript object literal. Emulating this with Java syntax would be too tedious (Scala frontend to GWT, please!), however, to ease this use case, I added a global $$ function for manipulation JavaScriptObject/JSON objects easily.
For example, if you want to create a JS object literal, you write:
Properties literal = $$("{ foo: 'bar', baz: 'bam'}");
You can then use this Properties literal in GQuery functions ala jQuery, or, you can access them with JS-like syntax
$("div").attr($$("{ foo: 1, bar: 2 }"); // set every DIV to have a foo attribute = 1, and bar attribute = 2
// And;
$$("{foo: 1, bar : 2}").get("foo") == 1;
$$("{foo: 1, bar: { baz: 3} }").get("bar.baz") == 3;
-Ray