We were having a discussion, the other day, on the jQuery-dev mailing list concerning style and jQuery code. There was some discussion about how it could be improved.
One of the points discussed was concerning the use of callbacks and jQuery(this). Callbacks (passing in a function as the final argument to a jQuery method, to be called later) are used all throughout jQuery code. It’s a rather core aspect of jQuery (especially the use of closures to pass around information).
We began to wonder: What would jQuery look like if there were no callbacks? A couple solutions were proposed but I posited one that, I think, promotes the idioms present in jQuery.
Its use is simple (although its implementation is definitely a mind-twister). Instead of using a callback you now using jQuery chaining to execute all the methods that you need.
Where previously you would’ve done:
jQuery("div").hide("slow", function(){ jQuery(this) .addClass("done") .find("span") .addClass("done") .end() .show("slow", function(){ jQuery(this).removeClass("done"); }); });
You would now write:
jQuery("div").chain("hide", "slow") .addClass("done") .find("span") .addClass("done") .end() .chain("show", "slow") .removeClass("done") .end() .end();
The end result is quite interesting. It definitely moves jQuery farther into the land of a “Domain Specific Language.” I wanted to put this code out there for people to try out and play with even though I have a number of reservations:
- I’m really not a fan of passing in a method name as a string argument, to another method. This is used a lot in Prototype.js but it just never sat right with me.
- The .chain() method name probably isn’t right. Some other name probably fits better here.
- The fact that you lose out on the power of normal JavaScript is daunting (you can no longer do something like: jQuery(this).html( jQuery(this).attr(“href”) ) – you’d have to go back to using a callback).
Nevertheless I think the result holds a lot of potential even if just as a proof-of-concept.
Chris (October 14, 2008 at 5:26 pm)
Personally, I think #3 is a show-stopper, but then again I’m not a JS ninja!
Mike Purvis (October 14, 2008 at 5:32 pm)
Yeah, it sure is cute, but too much of regular JS is lost, and not enough is gained. Especially if you start out implementing something this way and then have to convert it down the line because there’s one little bit that doesn’t work with the chaining.
Andrew Dupont (October 14, 2008 at 5:37 pm)
The method-name-as-string thing is used only once in Prototype that I can think of (Enumerable#pluck), but I’m probably forgetting something.
Anyway, I remember when you showed this to me a year and a half ago at SXSW and it BROKE MY HEAD. Anyway, I do agree that this is ultra-powerful, but looks too much like the sort of JavaScript I’m used to. If it looked different it could then be different. If that makes sense.
Remy Sharp (October 14, 2008 at 5:38 pm)
I like the idea that you can chain off an effect, and you don’t have to bother with the callback – where I’ve always wondered about the cost of creating a new jQuery instance of the element even though we had it in the first place for the effect (still with me?).
However, like you, it doesn’t quite sit right that we’re passing in a string to represent a function – it’s harder to spot when you’re skimming through code too.
For point #3, using a callback to do something that simple would definitely confuse, certainly me and perhaps new users.
Andrew Pendleton (October 14, 2008 at 5:43 pm)
Personally, I’m not a huge fan of this notation. I think the use of anonymous functions and closures is the biggest single thing that makes it possible to write really compact and yet fairly readable code in Javascript. Also, I find that the function notation makes it easier for me to distinguish, when reading code, between things that execute now and things that execute and some point on the future (after a timeout, or after some other event has occured), and I feel like this notation makes that distinction much less obvious. I’d be okay with making it an option, I guess, as long as it’s not mandatory, but personally, I don’t see any gain to using it.
Oh, and also, agreed that passing around function names is uber-hacky. It makes it feel like PHP in a not-good way.
Sugendran (October 14, 2008 at 5:52 pm)
I agree with @AndrewPendleton
How would you handle the non-jquery javascript that usually happens along the way? The example assumes that the callbacks only perform jQuery functions. Would functions then have to be rewritten to return the elements in order not to break the chain? And is this something that a library should impose on the coder?
I quite like the current method of chaining, it lets me write smaller code whilst still being flexible enough to quickly do strange and interesting things.
Rick Strahl (October 14, 2008 at 5:57 pm)
I’m not sure if this isn’t a step in the wrong direction. Sure it’s interesting from a language perspective, but what are you really gaining here and what are you losing?
Looks there’s a lot more to lose than gain, and if anything the syntax froces you to think more about what’s happening (in the chain() command) than simply being expressive with the full function parameter.
Avoiding a couple of curly brackets isn’t really worth the additional construct IMHO.
Ricardo Vega (October 14, 2008 at 6:10 pm)
For effects, this would be perfect:
$(element).fadeOut().remove()
better and simpler than
$(element).fadeOut(‘normal’,function(){ $(this).remove(); })
But make it optional! Ultra-chaining its a pleasure for simple task.
People that I’ve introduced to jQuery loves the way it is, very straight forward to define callbacks, its like “when this fires, this happens”.
Saludos a todos.
PS. What do you think about enabling some code-coloring to comments :)
Braden (October 14, 2008 at 7:08 pm)
I haven’t seen the discussion, so I apologize if I repeat, but wouldn’t something like this be feasible?
.show("slow", jQuery().removeClass, ["done"])
That is, if a reference to a jQuery function is passed, it passes forward its own
this
?Kris Kowal (October 14, 2008 at 7:09 pm)
I nominate “begin” instead of “chain”. Using “begin” and “end” stand in for curly braces as they do in Pascal and Delphi, and completes the symmetry of open and close curlies. Using “begin” and “end” makes it obvious that the chain of statements is masquerading for a code block.
Braden (October 14, 2008 at 7:14 pm)
Or [jQuery().removeClass, [“done”]], or {callback: jQuery.removeClass, args: [“done”]}, to avoid changing old APIs. In fact, I really like the object version.
Matt Kruse (October 14, 2008 at 8:25 pm)
What about syntax like this?
jQuery(“div”).hide(“slow”, $.callback().
.addClass(“done”)
.find(“span”)
.addClass(“done”)
.end()
.show(“slow”, $.callback().
.removeClass(“done”)
)
)
In this case, the $.callback function would have to return an object that has “queued up” the subsequent jQuery calls. Then when the callback is being evaluated, if it’s one of these objects instead of a function object, then it would be “un-rolled” and evaluated in context.
You lose the closure, but as long as I can still pass an actual function for the callback, I would use it if I needed the closure.
In the end, though, I’m not sure this kind of code is any better or clearer than passing functions.
Nick (October 14, 2008 at 8:53 pm)
Frankly, I think the benefits are far outweighed by the confusion and complication that you introduce. I can see the attraction of being able to chain on a few more things after an animation, but as you pointed out yourself, the function-name-as-string feature makes it feel extremely clunky.
Part of me wonders if there shouldn’t just be an additional option for the animation methods which cause them to block until the animation completes, then return the resultant jQuery object at the end. Though I appreciate that implementing this is probably an impressive headache in itself, probably requiring such things as busy waits (gasp, the horror).
Jed (October 14, 2008 at 10:05 pm)
#1 seems easy enough to mitigate by passing the functions themselves ($.fn.hide and $.fn.show).
Would it be possible to somehow provide a handler that would allow otherwise undefined callbacks to be defined _after_ a method is called? Something like the following seems very natural for me, but would probably require significant work (if it were possible at all):
jQuery("div")
.hide("slow")
.then()
.addClass("done")
.find("span")
.addClass("done")
.end()
.show("slow")
.then()
.removeClass("done")
.end()
.end();
Stefan (October 15, 2008 at 12:43 am)
The method name as argument is not required.
It would be possible to pass the actual methods as functions:
jQuery(“div”).chain(jQuery.hide, jQuery.hide.slow)
the chain method would then apply the methods with right context:
arguments[i].apply(this,[])
Stefan (October 15, 2008 at 12:46 am)
ERRATA:
jQuery(“div”).chain(jQuery.hide, “slow”)
function chain(){
…
var fn = arguments.shift();
fn.apply(this,arguments);
…
}
Malte (October 15, 2008 at 1:02 am)
Hey John,
very interesting post. I feel that it very important to define indentation rules for this kind of code. And something in the code needs to scream “This code is really asynchronous although it does not look like it”.
For another implementation of this concept, check out
http://code.google.com/p/joose-js/source/browse/trunk/lib/JooseX/DelayedExecution.js
I used that to turn the asynchronous HTML5 API usage into something that does not look like retarded LISP.
Rizqi Ahmad (October 15, 2008 at 2:25 am)
arrrgh, I have a plugin that use the name “chain” :( :(
Thomas Leveil (October 15, 2008 at 2:41 am)
I don’t like the chain thing… IMHO jQuery powers come from the selectors and the plugin thing… This is going to far away from javascript
Paul van Dam (October 15, 2008 at 2:56 am)
The chain method reminds me of Mootools. I think it creates more complexity than it solves and actually was one of the reasons I eventually moved to jQuery.
Anup (October 15, 2008 at 3:00 am)
John, this is quite interesting.
However, what I like about the ability to pass in anonymous functions is that they are more reusable, which then makes it easier to write
a) unit-testable code
b) plugins
c) more flexible plugins
anonymous (October 15, 2008 at 3:17 am)
Isnt this similar to arrows http://www.cs.umd.edu/projects/PL/arrowlets/ ?
Mark McDonnell (October 15, 2008 at 3:34 am)
I’m definitely not a JS ninja (hopefully John’s new book – secrets of a js ninja – will resolve that!) but I find the chaining method harder to understand than using a callback function and I feel like I lose too much of the core JavaScript language functionality which I enjoy writing!
William from Lagos (October 15, 2008 at 6:50 am)
Bad idea. Please scrap it. It’s rather confusing as you pointed out. This shouldn’t be implemented in jQuery
amix (October 15, 2008 at 7:13 am)
Great post. Another solution (that goes against the design of JQuery thought) is to use partial evaluation. The example would look this this:
chain(
$p(hide, elm, 'slow'),
$p(addClass, elm, 'done'),
$p(addClass, $gc(elm, 'span'), 'done'),
$p(chain,
$p(show, elm, 'slow'),
$p(removeClass, elm, 'done')
)
)
//$p = partial
//$gc = getChild
John Resig (October 15, 2008 at 8:26 am)
Thanks for the feedback, everyone! I just want to strongly emphasize that this is *not* going in to jQuery, is just an interesting experiment, and (as I outlined above) it has too many problems for me to be comfortable with.
@Andrew Dupont: The method I was thinking of, in particular, was .invoke().
@Kris Kowal: I like that name as well – thanks for the suggestion!
@Matt Kruse: I actually created something similar: Style 1 (similar to what you mentioned) and Style 2 (More generic and much simpler).
Lennie (October 15, 2008 at 8:26 am)
“3. The fact that you lose out on the power of normal JavaScript is daunting (you can no longer do something like: jQuery(this).html( jQuery(this).attr(“href”) ) – you’d have to go back to using a callback).”
John, I’m not a jquery user, so I’m not sure how these things are usually done, but why could you not do that ? In the function that implements it you can easily do: typeof arg1 == ‘string’.
riper (October 15, 2008 at 8:37 am)
jQuery(“div”).hide(“slow”, function(){
jQuery(this)
.addClass(“done”)
.find(“span”)
.addClass(“done”)
.end()
.show(“slow”)
.callback()
.removeClass(“done”)
.end(); // no required here
});
no?
Joe McCann (October 15, 2008 at 9:31 am)
I prefer the former to the latter; it makes more sense to me mentally to know that after the hide method is called and executed, additional code will be then executed. Semantically, I prefer that way than passing the function name as a string and “chaining” along the additional methods. My 2.
Ivan (October 15, 2008 at 9:55 am)
not a fan of methods as string names. i worry about future tooling and ides losing that valuable metadata when they read a function definition.
i’ll be sticking to the old style. it’s really readable imo and once a user learns how to author callbacks its really not that hard to do. plus, you typically dont even have so many anonymous functions; i find i place most callbacks in library of some sort and reference by a name.
Ivan (October 15, 2008 at 9:56 am)
another thought: isn’t this a slippery slope? how long before people place simple conditionals and others as strings… feels like its just placing the whole thing into an eval block.
Michael Schurter (October 15, 2008 at 1:03 pm)
Like some have said before me, #3 is a complete showstopper for me.
jQuery is whatever the JavaScript equivalent of “Pythonic” is.* Prototype is not. Please don’t ruin jQuery’s ability to elegantly integrate with plain old vanilla JavaScript.
* JavaScriptic? We need to work on this… ;)
7rans (October 15, 2008 at 3:40 pm)
Well, it’s an attempt at least. I very much dislike the Javascript notation –what Javascript really needs are Ruby-like blocks.
jQuery(“div”).hide(“slow”){
addClass(“done”);
find(“span”){
addClass(“done”);
}
show(“slow”){
removeClass(“done”);
}
}
But I don’t think there’s any way to do something like that.
Is there any way to “look ahead” for a chain? Then maybe:
jQuery(“div”).hide(“slow”).do
.addClass(“done”)
.find(“span”)
.addClass(“done”)
.end()
.show(“slow”).do
.removeClass(“done”);
.end
.end()
(I’m using “do” as a property rather than a function.)
I’m no expert, maybe this notation is not possible. But the notation seems much better if it is.
7rans (October 15, 2008 at 3:42 pm)
Lets try that again.
jQuery("div").hide("slow").do
.addClass("done")
.find("span")
.addClass("done")
.end()
.show("slow").do
.removeClass("done");
.end()
.end()
7rans (October 15, 2008 at 3:44 pm)
Err… where’s the preview button?
jQuery("div").hide("slow").do
.addClass("done")
.find("span")
.addClass("done")
.end()
.show("slow").do
.removeClass("done");
.end()
.end()
7rans (October 15, 2008 at 3:45 pm)
Forget it. Indent at your own leisure.
Chris Anderson (October 16, 2008 at 1:56 pm)
I’d miss the closures – I think jQuery does a good job balancing ease-of-use with “real” Javascript. By that I mean, that I’ve seen more than a few new developers come to a jQuery project, and really get their JS-chops by working on it. Understanding Javascript’s scope is one of the watershed moments for any aspiring JS-dev, and jQuery pushes users to understand closures at just the right level. Easy when you’re learning, and powerful when you need it.
@Kris – I like “begin” also.
yikulju (October 18, 2008 at 6:11 am)
I like the idea of making JavaScript more capable of behaving like a DSL.
Nick (October 18, 2008 at 7:54 am)
I prefer Prototype invoke syntax over this where functions themselves are chainable. Having something called chain() in combination with all those end()’s isn’t very intuitive, it gives too much code bloat.
john carpenter (October 19, 2008 at 1:14 pm)
fuck Js libraries.
Ricardo Tomasi (October 19, 2008 at 4:00 pm)
How about passing an argument to tell the next functions in the chain that they must wait?
Something like
$('div').fadeOut('slow','wait').remove();
In this case, fadeOut() would return the jQuery object with a boolean ‘wait’ flag attached. remove() (and every other function) checks for the flag, and if true attaches itself as a callback to the previous function (which in turn will look for these callbacks when it finishes if the argument ‘wait’ was passed).
Basically, every function that comes after fadeOut() enters a queue to be executed as a callback. Might create too much overhead/slow down the animation in long chains though.
Randall (October 19, 2008 at 6:17 pm)
It’s all about backward chaining.
Maybe we need a forward chaining:
jQuery(“div”).waitFor().hide(“slow”).
.addClass(“done”)
.find(“span”)
.addClass(“done”)
.end()
.waitFor().show(“slow”)
.removeClass(“done”)
.endWait()
.endWait();
waitFor() – executes 2nd forward element after the next element
endWait() – end of block
Forward chaining could raise the whole family of methods ie. waitFor(), waitWhile(), tee(), runIf(), runNtimes(), runWhile(), runParallel() etc.
john carpenter (October 19, 2008 at 7:36 pm)
and what about chainability?
ex: jQuery(“house”).goToToilet(“wait”)? endWait():return;
Randall (October 20, 2008 at 9:45 am)
@john carpenter: in your example endWait() is out of the chain. As I understand jQuery implementation, the purpose of chain is to keep the context of execution.
What I’m talking about is possibility to create pipes of commands. The semantics in jQuery could be very similar to that well known technique (cat file.txt | grep word | sort > results.txt).
I don’t mean it’s even needed for jQuery. It’s just possible. BTW pipes are cool again novadays. jQuery is cool even more!
Elijah Insua (October 20, 2008 at 10:11 pm)
@John Resig: Style 1 looks attractive to me and it doesn’t appear to be a huge change either.
Erik Harrison (October 21, 2008 at 4:27 pm)
Yeah, what Ricardo said. If there were an option to make effects only return when complete, then you’d get all of this benefit without making the ability to support closures go away.
Actually, that might be nice. I’d still probably implement code like the example using a callback, but it would simplify cases where I wanted to do the majority of my work after an animation has completed.
$('#ready').show('slow', 'wait');
//rest of my application
Unless I’m missing something that can currently be done?
Borre (October 22, 2008 at 1:40 pm)
This ‘ultra-chaining’ looks very powerful, but it will not work if arguments are to be passed to the callback function (i.e. in case of ajax functions like $.get).
Even if this can be fixed, it will never work on more complex functions that have more than one callback to them ($.ajax for example).
AFAIK the only other case in which callbacks are used is with effects. In this case, wouldn’t it be better just to have a wait() function that will wait until the effect finishes? Example:
jQuery('#ready').show('slow').wait().addClass('visible');
It can only be used on one effect/animation at a time, but I think it will eliminate most cases in which a callback is used.
Dmitriy (October 29, 2008 at 1:09 pm)
I like the idea of a “then” method which will trigger the chain.
jQuery(this)
.show(“slow”).then()
.foo()
.bar()
.foobar()
There should be NO END for the then since can you imagine reading the execution path for the following code:
jQuery(this).show(“slow”)
.then()
.foo()
.bar()
.end()
.foobar();
The execution path would be:
show > foobar
plus an asynchronous foo > bar after show completes.
This will make chaining horribly ugly. Any proposed solution the the comments that requires an end() is a no in my opinion.
James Coglan (May 9, 2009 at 5:49 pm)
I’m a little late to the game on this one but the topic seems to be doing the rounds again. I published a general-purpose method for storing chains of method calls and replaying them a while ago and have been using it in Ojay ever since. Ojay allows asynchronous chaining for event handling, delayed execution, animation sequencing and Ajax callbacks. The library initially came about because we wanted something in the style of jQuery but with much easier async programming built in. More information, if anyone’s interested:
http://blog.jcoglan.com/2007/10/30/asynchronous-function-chaining-in-javascript/
http://jsclass.jcoglan.com/methodchain.html
http://ojay.othermedia.org/articles/method_chain.html