6.7. AJAX: Asynchronous JavaScript And XML

In 1998, Microsoft added a new function to the JavaScript global object defined by Internet Explorer 5. XmlHttpRequest (usually shortened to XHR) allowed JavaScript code to initiate HTTP requests to a server without loading a new page and use the server’s response to modify the DOM of the current page. This new function, key to AJAX apps, allowed creating a rich interactive UI that more closely resembled a desktop application, as Google Maps powerfully demonstrated. Happily, you already know all the ingredients needed for “AJAX on Rails” programming:

  1. Create a controller action or modify an existing one (Section 4.4) to handle the AJAX requests made by your JavaScript code. Rather than rendering an entire view, the action will render a partial (Section 5.1) to generate a chunk of HTML for insertion into the page.

  2. Construct your RESTful URI in JavaScript and use XHR to send the HTTP request to a server. As you may have guessed, jQuery has helpful shortcuts for many common cases, so we will use jQuery’s higher-level and more powerful functions rather than calling XHR directly.

  3. Because JavaScript is by definition single-threaded —it can only work on one task at a time until that task completes—the browser’s UI would be “frozen” while JavaScript awaited a response from the server. Therefore XHR instead returns immediately and lets you provide an event handler callback (as you did for browser-only programming in Section 6.6) that will be triggered when the server responds or an error occurs.

  4. When the response arrives at the browser, your callback is passed the response content. It can use jQuery’s replaceWith() to replace an existing element entirely, text() or html() to update an element’s content in place, or an animation such as hide() to hide or show elements, as Figure 6.8 showed. Because JavaScript functions are closures (like Ruby blocks), the callback has access to all the variables visible at the time the XHR call was made, even though it executes at a later time and in a different environment.

Let’s illustrate how each step works for an AJAX feature in which clicking on a movie title shows the movie details in a floating window, rather than loading a separate page. Step 1 requires us to identify or create a new controller action that will handle the request. We will just use our existing MoviesController#show action, so we don’t need to define a new route. This design decision is defensible since the AJAX version of the action performs the same function as the original version, namely the RESTful “show” action. We will modify the show action so that if it’s responding to an AJAX request, it will render the simple partial in Figure 6.13(a) rather than an entire view. You could also define separate controller actions exclusively for AJAX, but that might be non-DRY if they duplicate the work of existing actions.

<p> <%= movie.description %> </p>

<%= link_to 'Edit Movie', edit_movie_path(movie), :class => 'btn btn-primary' %>
<%= link_to 'Close', '', :id => 'closeLink', :class => 'btn btn-secondary' %>
class MoviesController < ApplicationController
    def show
        id = params[:id] # retrieve movie ID from URI route
        @movie = Movie.find(id) # look up movie by unique ID
        render(:partial => 'movie', :object => @movie) if request.xhr?
        # will render app/views/movies/show.<extension> by default
    end
end
Figure 6.13: (a) Top: a simple partial that will be rendered and returned to the AJAX request. We give the “Close” link a unique element ID so we can conveniently bind a handler to it that will hide the popup. (b) Bottom: The controller action that renders the partial, obtained by a simple change to Figure 4.5: if the request is an AJAX request, line 5 performs a render and immediate return. The :object option makes @movie available to the partial as a local variable whose name matches the partial’s name, in this case movie. If xhr? is not true, the controller method will perform the default rendering action, which is to render the show view as usual.

How does our controller action know whether show was called from JavaScript code or by a regular user-initiated HTTP request? Fortunately, every major JavaScript library and most browsers set an HTTP header X-Requested-With: XMLHttpRequest on all AJAX HTTP requests. The Rails helper method xhr?, defined on the controller instance’s request object representing the incoming HTTP request, checks for the presence of this header. Figure 6.13(b) shows the controller action that will render the partial.

Moving on to step 2, how should our JavaScript code construct and fire off the XHR request? We want the floating window to appear when we click on the link that has the movie name. As Section 6.6 explained, we can “hijack” the built-in behavior of an element by attaching an explicit JavaScript click handlertoit. Of course, for graceful degradation, we should only hijack the link behavior if JavaScript is available. So following the same strategy as the example in Section 6.6, our setup function (lines 2–8 of Figure 6.14) binds the handler and creates a hidden div to display the floating window. Legacy browsers won’t run that function and will just get the default behavior of clicking on the link.

The actual click handler getMovieInfo must fire off the XHR request and provide a callback function that will be called with the returned data. For this we use jQuery’s ajax function, which takes an object whose properties specify the characteristics of the AJAX request, as lines 10–15 of Figure 6.14 show. Our example shows a subset of the properties you can specify in this object; one important property we don’t show is data, which can be either a string of arguments to append to the URI (as in Figure 3.2) or a JavaScript object, in which case the object’s properties and their values will be serialized into a string that can be appended to the URI. As always, such arguments would then appear in the params[] hash available to our Rails controller actions.

 1var MoviePopup = {
 2    setup: function() {
 3        // add hidden 'div' to end of page to display popup:
 4        let popupDiv = $('<div id="movieInfo"></div>');
 5        popupDiv.hide().appendTo($('body'));
 6        $(document).on('click', '#movies a', MoviePopup.getMovieInfo);
 7    }
 8    ,getMovieInfo: function() {
 9        $.ajax({type: 'GET',
10                url: $(this).attr('href'),
11                timeout: 5000,
12                success: MoviePopup.showMovieInfo,
13                error: function(xhrObj, textStatus, exception) { alert('Error!'); }
14                // 'success' and 'error' functions will be passed 3 args
15            });
16        return(false);
17    }
18    ,showMovieInfo: function(data, requestStatus, xhrObject) {
19        // center a floater 1/2 as wide and 1/4 as tall as screen
20        let oneFourth = Math.ceil($(window).width() / 4);
21        $('#movieInfo').
22        css({'left': oneFourth,  'width': 2*oneFourth, 'top': 250}).
23        html(data).
24        show();
25        // make the Close link in the hidden element work
26        $('#closeLink').click(MoviePopup.hideMovieInfo);
27        return(false);  // prevent default link action
28    }
29    ,hideMovieInfo: function() {
30        $('#movieInfo').hide();
31        return(false);
32    }
33};
34$(MoviePopup.setup);
Figure 6.14: The ajax function constructs and sends an XHR request with the given characteristics. type specifies the HTTP verb to use, url is the URL or URI for the request, timeout is the number of milliseconds to wait for a response before declaring failure, success specifies a function to call with the returned data, and error specifies a function to call if a timeout or other error occurs. Many more options to the ajax function are available, in particular for more robust error handling.

Looking at the rest of the code in Figure 6.14, getting the URI that is the target of the XHR request is easy: since the link we’re hijacking already links to the RESTful URI for showing movie details, we can query its href attribute, as line 10 shows. Lines 12–13 remind us that function-valued properties can specify either a named function, as success does, or an anonymous function, as error does. To keep the example simple, our error behavior is rudi- mentary: no matter what kind of error happens, including a timeout of 5000 ms (5 seconds), we just display an alert box. In case of success, we specify showMovieInfo as the callback.

#movieInfo {
    padding: 2ex;
    position: absolute;
    border: 2px double grey;
    background: wheat;
}
Figure 6.15: Adding this code to app/assets/stylesheets/application.css specifies that the “floating” window should be positioned at absolute coordinates rather than relative to its enclosing element, but as the text explains, we don’t know until runtime what those coordinates should be, so we use jQuery to dynamically modify #movieInfo’s CSS style properties when we are ready to display the floating window.

Some interesting CSS trickery happens in lines 20 and 23 of Figure 6.14. Since our goal is to “float” the popup window, we can use CSS to specify its positioning as absolute by adding the markup in Figure 6.15. But without knowing the size of the browser window, we don’t know how large the floating window should be or where to place it. showMovieInfo computes the dimensions and coordinates of a floating div half as wide and one-fourth as tall as the browser window itself (line 20). It replaces the HTML contents of the div with the data returned from the server (line 22), centers the element horizontally over the main window and 250 pixels from the top edge (line 23), and finally shows the div, which up until now has been hidden (line 24).

There’s one last thing to do: the floated div has a “Close” link that should make it disappear, so line 26 binds a very simple click handler to it. Finally, showMovieInfo returns false (line 27). Why? Because the handler was called as the result of clicking on a link (<a>) element, we need to return false to suppress the default behavior associated with that action, namely following the link. (For the same reason, the “Close” link’s click handler returns false in line 31.)

With so many different functions to call for even a simple example, it can be hard to trace the flow of control when debugging. While you can always use console.log( string ) to write messages to your browser’s JavaScript console window, it’s easy to forget to remove these in production, and as Chapter 8 describes, such “printf debugging” can be slow, inefficient and frustrating. In Section 6.8 we’ll introduce a better way by creating tests with Jasmine.

Lastly, there is one caveat we need to mention which could arise when you use JavaScript to dynamically create new elements at runtime, although it didn’t arise in this particular example. We know that $(’.myClass’).on(’click’,func) will bind func as the click handler for all current elements that match CSS class myClass. But if you then use JavaScript to create new elements matching myClass after the initial page load and initial call to on, those elements won’t have the handler bound to them, because on can only bind handlers to already-existing elements.

A common solution to this problem is to take advantage of a jQuery mechanism that allows an ancestor element to delegate event handling to a descendant, by using on’s polymorphism: $(’body’).on(’click’,’.myClass’,func) binds the HTML body element (which always exists) to the click event, but delegates the event to any descendant matching the selector .myClass. Since the delegation check is done each time an event is processed, new elements matching .myClass will “automagically” have func bound as their click handler when created.

6.16
Figure 6.16: Comparison of setting up and using Jasmine and RSpec. All paths are relative to the app root and all commands should be run from the app root. As you can see, the main difference is the use of lower_snake_case for filenames and method names in Ruby, versus lowerCamelCase in JavaScript.

Self-Check 6.7.1. In line 13 of Figure 6.14, why did we write MoviePopup.showMovieInfo instead of MoviePopup.showMovieInfo() ?

The former is the actual function, which is what ajax expects as its success property, whereas the latter is a call to the function.

Self-Check 6.7.2. In line 33 of Figure 6.14, why did we write $(MoviePopup.setup) rather than $(’MoviePopup.setup’) or $(MoviePopup.setup()) ?

We need to pass the actual function to $(), not its name or the result of calling it.

Self-Check 6.7.3. Continuing Self-Check 6.7.2, if we had accidentally called $(’MoviePopup.setup’) , would the result be a syntax error or legal but unintended behavior?

Recall that $() is overloaded, and when called with a string, it tries to interpret the string as HTML markup if it contains any angle brackets or a CSS selector otherwise. The latter applies in this case, so it would return an empty collection, since there are no elements whose tag is MoviePopup and whose CSS class is setup.