The Bungee Blog

News, updates and rants around The Bungee Book (the landmark book on Prototype and script.aculo.us)

Archive for the 'Articles' Category

Getting Out of Binding Situations in JavaScript

I’m delighted to report that my article in renowned A List Apart just went out, and it covers a pretty tricky aspect of JavaScript often misunderstood or poorly known: binding.

Head over to ALA and enjoy: Getting Out of Binding Situations in JavaScript.

2 comments

Rich autocompletion

A few days ago, an astute reader of the book, Bharat Ruparel, requested an example of multiple-update autocompletion on the book’s forums. I thought it would make for a nice demo page and a post here, so here you go, Bharat.

The main idea behind autocompletion with script.aculo.us is that the possible results are sent as a <ul>/<li> list, one list item per result, and the full textual contents of an item (including line breaks and whitespace), except whatever contents sits inside an element with a CSS class named “informal,” are extracted to provide the completed text.

This behavior can be altered, however, mainly through two means:

  • Using the select option to provide a CSS class name marking which textual contents to use (any other textual contents will be ignored)
  • Providing a custom extraction logic in addition to the default one through the afterUpdateElement callback.

Both these are discussed in the book (chapter 16, which is 20 pages), but the callback approach isn’t demonstrated in-depth. For this post, I prepared a detailed demo page that I invite you all to go through. It contains detailed explanative material around each step, from the most basic call to the full-fledged one, and tries to demonstrate the snags you can hit and how to achieve more advanced completion.

Check out the full demo page!

I hope this helps, and perhaps even provides a few of you with that “Aha!” moment I cherish.

Cheers,

No comments

Mixing argumentNames and wrap for varargs-like sweetness

Back in the first Neuron Workout solutions, I suggested an exercise for implementing varargs-like functionality using two of Prototype’s extensions to Function: argumentNames and wrap. I also promised I would post a possible solution here.

The idea was to provide a mechanism that would take an object or class in, and alter any method ending with a parameter named anythingElse so that such methods would end up with any “remaining” arguments stored as an array in this special parameter.

As promised, here’s the code for it:

10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Object.enableVarArgs = function(object) {
  for (var methodName in object) {
    var method = object[methodName];
    if (!Object.isFunction(method)) continue;
    (function() {
      var argNames = method.argumentNames();
      if (argNames.last() != 'anythingElse') return;
      object[methodName] = method.wrap(function() {
        var args = $A(arguments), proceed = args.shift();
        args[argNames.length - 1] = args.slice(argNames.length - 1);
        args.length = argNames.length;
        return proceed.apply(this, args);
      });
    })();
  }
};
 
Class.enableVarArgs = function(klass) {
  return Object.enableVarArgs(klass.prototype);
};

Before diving too much into the inner workings of this, let’s put together a demo object, reuse it for a demo class, and see whether this all works:

31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
var obj = {
  method1: function(a, b) {
    console.log("CALLED: method1(" +
      Object.inspect(a) + ', ' +
      Object.inspect(b) + ")");
  },
 
  someMethod: function(a, b, anythingElse) {
    console.log("CALLED: someMethod(" +
      Object.inspect(a) + ", " + Object.inspect(b) + ", " +
      Object.inspect(anythingElse) + ")");
  },
 
  method2: function(anythingElse) {
    console.log("CALLED: method2(" +
      Object.inspect(anythingElse) + ")");
  }
};
 
var Klass = Class.create(obj);
var obj2 = new Klass();
 
Class.enableVarArgs(Klass);
Object.enableVarArgs(obj);
 
obj.method1(1, 2, 3, 4);
obj.someMethod(1, 2, 3, 4);
obj.method2(1, 2, 3, 4);
obj2.method1(1, 2, 3, 4);
obj2.someMethod(1, 2, 3, 4);
obj2.method2(1, 2, 3, 4);

I used Firebug’s console object here, but you may use interactive alert calls just as well. On my browser, the output goes like this:

CALLED: method1(1, 2)
CALLED: someMethod(1, 2, [3, 4])
CALLED: method2([1, 2, 3, 4])
CALLED: method1(1, 2)
CALLED: someMethod(1, 2, [3, 4])
CALLED: method2([1, 2, 3, 4])

Okay, so how does this work?

Wel, we start by iterating over the object’s methods. This can easily be done by using a regular for…in loop to grab all the properties and filter using Prototype’s Object.isFunction predicate (which is just a typeof test).

11
12
13
  for (var methodName in object) {
    var method = object[methodName];
    if (!Object.isFunction(method)) continue;

Now the major trick was to avoid scope/reference issues with the remainding variables, most importantly argNames.

Because of JavaScript scoping issues, argNames will usually be a shared reference for all iterations of that loop. For instance, consider our test object: two of its methods match our criteria: someMethod, with argument names a, b and anythingElse, and method2, with just anythingElse. Because of this, the method replacement code (the one used in our wrap call) for someMethod will end up using the value of argNames that was set by processing method2, resulting in a wrecked final arguments list.

The traditional way of coping with such situations is to give such variables a guaranteed private scope, which is achieved by creating an anonymous function for the scope and calling it right after it’s defined (a technique used for implementing the famous module pattern, too).

This is why we wrap the remainder of our code inside this anonymous function, which we then call immediately (hence the weird )() code fragment).

14
15
16
17
18
19
20
21
22
23
    (function() {
      var argNames = method.argumentNames();
      if (argNames.last() != 'anythingElse') return;
      object[methodName] = method.wrap(function() {
        var args = $A(arguments), proceed = args.shift();
        args[argNames.length - 1] = args.slice(argNames.length - 1);
        args.length = argNames.length;
        return proceed.apply(this, args);
      });
    })();

So on to the code inside this anonymous function.

We start by filtering more on the last argument name, to check whether it is indeed anythingElse. As we’re not in the lexical scope of the surrounding for loop now, we can’t just call continue, we need to return from the anonymous function (since the loop has no code after this function, it’s pretty much equivalent).

15
16
      var argNames = method.argumentNames();
      if (argNames.last() != 'anythingElse') return;

Then we replace the former code for the method by our new code, which wraps the original one. Wrapped methods always received the former version as their first argument, which we traditionally call proceed. To easily tweak the arguments actually passed to our new methods, we grab the JavaScript predefined arguments variable, pass it to $A to make sure we have a proper, full-fledged Array to play with, and take the first argument out as our original method (proceed).

18
        var args = $A(arguments), proceed = args.shift();

All that is left to do is put all arguments from the anythingElse-positioned one to the end of the actual argument list in an array to be passed instead of anythingElse, and “cut” the arguments array to the proper length:

19
20
        args[argNames.length - 1] = args.slice(argNames.length - 1);
        args.length = argNames.length;

The actual call is performed by the native apply method on our original function, which has the good sense of taking the arguments as an array (which makes it ideal for our purposes). Of course, if there was an original returned value, we need to propagate it by returning it too.

20
        return proceed.apply(this, args);

Feel free to ask your questions in the comments, and discuss your own solutions if you had worked out any!

You can also download the full demo script for this example and play with it (you’ll need prototype.js loaded, obviously).

Also note I’m working on the second installment of the Neuron Workout Solutions, so stay tuned!

No comments

Podcast at Prag’s

If you’d like to hear me talk about why raw JavaScript and raw DOM interfaces are not enough, and how Prototype and script.aculo.us make it all nicer to play with, check out the fresh podcast at Pragmatic Programmers.

Sorry for the average audio quality on my side of the interview: it was done through Skype…

Let me know what you think!

No comments