Archive for December, 2011

Python Calling Javascript? A UI Pattern

Thursday, December 22nd, 2011

We have been very busy (excuse us for the lack of posts!) working on a second version of a client project. It involves redoing the whole UI in a much more responsive manner. In times past the “simple” way I have done this, especially with Django on the backend, has been to have the server respond with HTML, and inject that into the DOM. There are some serious limitations with this approach once you get far enough down the road. And it gets very messy.

We decided on a whole different approach to a responsive application. We are serving up data. The javascript takes care of the rest. Often times a single action requires multiple reactions by the interface. Yet, if you’ve abstracted your code (be pythonic, even in javascript: D.R.Y!) you can’t jump into a specific call/response, or even if you could, it’d be awful to throw a switch statement in there. The Command Pattern to the rescue!

To be fair, what we’ve implemented is a very simple execution of the pattern. Let’s take the python-end first:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class CommandBuilder(object):
 
    def __init__(self, *args, **kwargs):
        super(CommandBuilder, self).__init__(*args, **kwargs)
        self.data = []
 
    def raw_callback(self, callback, args):
        self.data.append({ 'callback': callback, 'args': args })
        return self
 
    @property
    def js(self):
        return self.JavascriptMethods(self)
 
    def dumps(self):
        return { 'commands': self.data }
 
    class JavascriptMethods(object):
 
        def __init__(self, command):
            self.command = command
 
        def a_function(self, a_model):
            return self.command.raw_callback('a_function', [ a_model.id, a_model.name ])

Just for some syntactic-sugar I also used the builder pattern and a python property which masks the Javascript Methods inner class. All that means you can write pretty code like this:

1
2
cmds = CommandBuilder.js.a_function(a_model).a_function(a_model).a_function(a_model)
return HTTPResponse(simplejson.dumps(cmds.dumps()))

In the real world you wouldn’t be calling the same javascript function over and over, you would expose the functions you need to call. And now we need a little bit of javascript to handle this known JSON data structure:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var Commander = {
    a_function: function(id, name) {
        alert(name+" has an id of: "+id);
    }
};
 
var Engine = {
    run_command: function(data) {
        if(data == undefined || data.command == undefined || data.command.length === 0) {
            alert('This is not a command structure');
        }
        for(var i in data.command) {
            var row = data.command[i];
            if(typeof Commander[row.callback] === 'function') {
                 Commander[row.callback].apply(undefined, row.args); // undefined as this
            } else {
                 alert('Function '+row.callback+' does not exist on Commander');
            }
        }
    }
};

The javascript `apply` method is a lifesaver here. With this simple pattern we can have the server force client-side code to be executed. Now, if you’ve been a diligent developer you’ve already been naming your functions on the client-side, not just throwing around a million anonymous functions. And you’ve separated out the functions that deal with calling URLs or dealing with the DOM from those functions which deal with the data. There is a huge bonus with doing that (besides this pattern being useful for you): you can start testing your javascript!

Reflections:

  • I tend to think of the `.js` in python a namespace. You can specify a specific Javascript object on the front end with a few minor changes on both ends. My `CommandBuilder` object has a few more methods on it, so the `.js` serves as a semantic separator as well.
  • There is only one downside to this pattern that I have observed so far – if you make changes to function signatures in javascript, you’ll need to change the python side as well. You need to stay on your toes with this pattern.