Running jQuery Code From Ruby Using ExecJS-Async

Last week I wrote a post about using the Asset Pipeline outside of Rails because we needed to test answers for Code School’s upcoming Zombies 2 course. But we also need a way to test JavaScript (that uses jQuery) that manipulates a DOM, and all from inside of Ruby.

ExecJS to the Rescue

ExecJS is a Ruby gem that lets you run JavaScript code from Ruby. It does this by using the best JavaScript runtime available to evaluate JavaScript and returning the result in Ruby! For example:

ExecJS.eval "'red yellow blue'.split(' ')"
# => ["red", "yellow", "blue"]

Rad! Or you can use it to compile a block of JavaScript and then call it later, like this:

context = ExecJS.compile "var run = function(foo) { return foo + foo }"
context.call 'run', 'bar'
# => 'barbar'

The Asset Pipeline uses ExecJS for it’s CoffeeScript compilation and JavaScript minification. But we still needed to somehow programmatically setup a DOM, load the jQuery JavaScript library, and inject our student’s JavaScript.

Luckily, we’d already solved this problem for our jQuery Air and HTML5 & CSS3 Functional Design courses, using the JSDOM JavaScript library. JSDOM makes it super easy to create a DOM and require external javaScript libraries (like jQuery). You can supply your own HTML and then manipulate it in JavaScript using the jsdom.env, method, like this:

html = '<h1>JSDOM Homepage</h1>'
jquery = 'http://code.jquery.com/jquery-1.5.min.js'
jsdom.env(html, [jquery], function(errors, window) {
  console.log("contents of a.the-link:", window.$("h1").text());
  // logs: JSDOM Homepage
});

So we went to work hooking all these pieces up together, but we hit a snag early on. We wanted to setup JSDOM with some fixture HTML data, require jQuery, then run the student’s JavaScript and finally return the modified DOM, like this:

jsdom.env(html, [jquery], function(errors, window) {
  var jQuery = window.jQuery;
  var $ = jQuery;
  var document = window.document;
  eval(students_code);
  return $('body').html();
});

But jsdom.env works asynchronously, returning before the function callback is run. And ExecJS does not work with asynchronous JavaScript:

context = ExecJS.compile <<-JAVASCRIPT
  var run = function(html, code){
    jsdom.env(html, [jquery], function(errors, window) {
      var jQuery = window.jQuery;
      var $ = jQuery;
      var document = window.document;
      eval(students_code);
      return window.jQuery('body').html();
    });
  }
JAVASCRIPT

html = "<h1>Foo</h1>"
student_code = "$('h1').text('Bar')"
context.call 'run', html, student_code
# => ""

Digging into the ExecJS code (and then talking to the maintainers) it was clear that ExecJS would only ever work with synchronous JavaScript code because ExecJS’s goal is to provide a common interface against a number of different JavaScript runtimes, and the runtimes all have different ways (or none at all) of doing async callbacks. But since we’d always be running the Node.js runtime (since we use Heroku), we wrote a gem that adds async support to ExecJS for the Node runtime only, called execjs-async. Now this works:

context = ExecJS.compile_async <<-JAVASCRIPT
  var run = function(html, code){
    jsdom.env(html, [jquery], function(errors, window) {
      var jQuery = window.jQuery;
      var $ = jQuery;
      var document = window.document;
      eval(students_code);
      callback(window.jQuery('body').html());
    });
  }
JAVASCRIPT

html = "<h1>Foo</h1>"
student_code = "$('h1').text('Bar')"
context.call 'run', html, student_code
# => "<h1>Bar</h1>"

Notice that, instead of using return, we are calling a function named callback and whatever we pass to that will be the result of calling context.call. ExecJS-Async provides this callback function to any javascript code compiled with ExecJS.compile_async.

This is a stripped down version of the code we are using for Zombies 2, but it’s pretty close. Zombies 2 is almost ready to go, you can signup here to be notified when it becomes available.


Eric Allam
Eric Allam

Filed under: Build

Next article →

Try Ruby Rewrite & Redesign

Gregg Pollack Gregg Pollack