Thursday, May 24, 2012

using JQuery functionally

I was looking at the code on a site recently and it was all blocks of JQuery. The first thing I saw was that there was a lot of lines that were almost the same - basically that there was a selector then 3-4 methods chained off it. First thing I thought was why not put that in a function, something like:

var doStuff = function(selector) {
$(selector).method1().method2().method3();
};

which would allow you just to call the doStuff method with the selector. It then hit me that we're just abstracting away the functionality of JQuery which is a good thing. Then I wondered if I could change it in to functional style (without writing the whole of JQuery).


The first thing is to realize is that Javascript allows us to change scope using call or apply. You can pretty much write OO code in a functional style using those. So basically these are the same:

// OO
obj.method(param);

// Functional
Obj.prototype.method.call(obj, param);

Now JQuery makes it easy to get to it's prototype for adding plugins. This mean we can call a jquery method like:

$.fn.method.call($(selector));

Ah, now we can see that the $() is a function itself. So now we can use composition f.g(x) == f(g(x)):

compose($.fn.method, $)(selector);

There is still a problem though, compose will call the method with a different context and pass in the JQuery object as the first argument. There are two solutions and they're both fairly useful. The first is that we could have a special bind for JQuery methods:

var $bind = function(fn) {
  var args = arguments;
  return function($object) {
    return fn.apply($object, args.splice(1));
  };
};

so we could bind the method and pass it in to compose or even just use it:

var $method = $bind(method);
var $elems = $(selector);
$method($elems);

// or

compose($method, $)(selector);

The major difference is that $bind will take in other arguments when bound, whereas compose methods only accept one argument.

But we can probably do on better. We could just check to see if it's a jquery function and apply it inside compose. So let's make $compose:

var $compose = function(var_args) {
  var args = [].slice.call(arguments);
  return function(arg) {
    var ret = arg;
    for (var i = args.length; i; i--) {
      if(args[i - 1] in JQuery.fn)
        ret = $bind(args[i - 1])(ret);
      else
        ret = args[i - 1](ret);
    }
    return ret;
  };
};

Now we can just pass in the jquery methods to compose as they are. So let's compare:

// OO style
var doStuff = function(selector) {
  $(selector).method1().method2().method3();
};

doStuff(selector);

// Functional style
var doStuff = $compose($.fn.method3, $.fn.method2, $.fn.method1, $);

doStuff(selector);

So there we have it. JQuery being used functionally. I haven't tested or tried any of the code so not sure if it will just work but hopefully it's enough to get you interested and I'd love to see what other people can do with it. Keep in mind though that compose functions take one argument so it's great for things like $.fn.hide but you may want to bind other methods:

var makeRed = $bind($.fn.css, {'background': 'red'});
compose(makeRed, $)(selector);

You may want to make some improvements in $bind and $compose like automatically wrap whatever is passed in with the JQuery object.

2 comments: