If you’re using prototype.js for Ajax and updating your page with HTML that defines new javascript functions, you’re likely to encounter issues. One solution is to pull the function definitions out of the HTML to a javascript file that is loaded when the page first loads. More often than not this is the correct solution. However, understanding the problem is quite instructive and leads to another solution that can be used in a pinch when pulling the function out is not an option. ...

If you’re using prototype.js for Ajax and updating your page with HTML that defines new javascript functions, you’re likely to encounter issues. One solution is to pull the function definitions out of the HTML to a javascript file that is loaded when the page first loads. More often than not this is the correct solution. However, understanding the problem is quite instructive and leads to another solution that can be used in a pinch when pulling the function out is not an option.

To set the stage, let’s suppose we have a Rails application with a view that has a button. When that button is clicked, an Ajax request is made that results in another button being added to the page. The new button, in turn, triggers a call to a javascript function that happens to also be defined in the response to the Ajax request. Let’s see what happens…

index.rhtml:

  <html>
    <head>
      <%= javascript_include_tag :defaults %>
    </head>
    <body>
      <h1>Click the Button!</h1>

      <button onclick="<%= remote_function :url => {:action => 'new_button'},
                                           :update => 'new_button_target' %>">
        Get New Button
      </button>

      <div id="new_button_target"></div>
    </body>
  </html>

new_button.rhtml:

  <h2>Here's a new button!</h2>

  <script type="text/javascript">
    function prove_function_defined() {
      alert("You called?");
    }
  </script>
  <button onclick="prove_function_defined()">Call Function</button>

Loading the index page in Safari we see what we expect:

Clicking the button seems to work fine too:

But when we click the new button we get no alert. Instead we get this:

Usually the first reaction here is to assume that the javascript in the Ajax response is never evaluated. Unfortunately the problem isn’t so simple. We can prove the javascript is evaluated by adding a simple alert.


  ...
  <script type="text/javascript">
    alert("Really, I'm getting defined!");
    function prove_function_defined() {
      alert("You called?");
    }
  </script>
 ...

Now when we click the “Get New Button” button an alert is shown:

The function definition directly follows the alert that shows up, so the function must be getting defined. If so, where is it? We already know it isn’t directly accessible from the onclick on our button. If we dig far enough down into prototype.js, we find that the answer lies in how the javascript from the Ajax response is evaluated.


  Element.Methods = {
    ... 
    update: function(element, content) {
      ...
      element.innerHTML = content.stripScripts();
      content.evalScripts.bind(content).defer();
      ...
    },
    ..
  };

The update function here is the function that prototype calls to update the new_button_target div in index.rhtml. I’ve eliminated the irrelevant parts here to highlight the two lines that matter. Prototype strips out the script tags before setting the innerHTML property of the element. It then sets a timeout (the call to defer) causing the browser to invoke the evalScripts function a little later and continues on it’s merry way. So to understand where our function is being defined, we need to look at evalScripts.


  evalScripts: function() {
    return this.extractScripts().map(function(script) { return eval(script) });
  }

evalScripts extracts the script tags from the html fragment and then calls eval on each one of them. Now we finally come to the crux of it—eval evaluates the script passed to it in the current context, which is within the evalScripts function. Unfortunately for us, javascript allows functions to be defined as part of the local scope of a another function, much like local variables. So our function is being defined, but local to a particular execution of evalScripts. As soon as evalScripts returns, our function is lost. Oh no!

This is where we would probably decide to just pull the function out of the HTML being rendered for the Ajax response and make sure it’s loaded when index.rhtml was loaded, but we might run into a situation where this is highly inconvenient. If we find ourselves in such a situation, we can use our knowledge of javascript functions, variables, and scopes to worm our way out. In javascript functions are nothing more than variables that hold function objects. In fact, the syntax for defining a function in javascript is little more than syntactic sugar. We could just as easily assign a variable to a function object ourselves. Furthermore, assignments within a function in javascript create a global variable if you do not include the var keyword, which would make it local. Therefore we can force our function to be defined globally simply by changing the code like this.


  ...
  <script type="text/javascript">
    prove_function_defined = function() {
      alert("You called?");
    }
  </script>
  ...

Now when we reload the index page, click on the first button and then the second we get the behavior we want!

Whew… all that javascript hurts my head. I’m going to go lay down for awhile. Have a good night!

Sorry, comments are closed for this article.