6.10. Single-Page Apps and JSON APIs

Google Maps was an early example of the emerging category called client-side single-page apps (SPAs). In a SPA, after the initial page load from the server, all interaction appears to the user to occur without any page reloads. While we won’t develop a full SPA in this section, we will show the techniques necessary to do so.

So far, we have concentrated on using JavaScript to enhance server-centric SaaS apps; since HTML has long been the lingua franca of content served by those apps, rendering a partial and using JavaScript to insert the “ready-made” partial into the DOM was a sensible way to proceed. But with SPAs, it’s more common for client-side code to request some “raw” data from the server, and use that data to construct or modify DOM elements. How can a Rails app return raw data rather than HTML markup to JavaScript client code?

One simple mechanism is for the controller action to use render :text to return a plain string. But what if we need to send structured data to the client? As you have probably guessed, JSON solves that problem, even though the X in AJAX stands for XML, which was originally believed to be the standard that would take hold for data interchange. In practice, modern browsers’ JSAPIs include a function JSON.parse that converts a string of JSON into the corresponding JavaScript object(s).

To use JSON in our client-side code, we must address three questions:

  1. How do we get the server app to generate JSON in response to AJAX requests, rather than rendering HTML view templates or partials?

  2. How does the client specify that it expects a JSON response, and how does it use the JSON response data to modify the DOM?

  3. When testing AJAX requests that expect JSON responses, how can we use fixtures to “stub out the server” and test these behaviors in isolation, as we did in Section 6.8?

The first question is easy. If you have control over the server code, your Rails controller actions can emit JSON rather than an XML or HTML template by using render :json=> object, which sends a JSON representation of an object back to the client as the single response from the controller action. Like rendering a template, you are only allowed a single call to render per action, so all the response data for a given controller action must be packed into a single JSON object.

 1let MoviePopupJson = {
 2    // 'setup' function omitted for brevity
 3    getMovieInfo: function() {
 4        $.ajax({type: 'GET',
 5                dataType: 'json',
 6                url: $(this).attr('href'),
 7                success: MoviePopupJson.showMovieInfo
 8                // 'timeout' and 'error' functions omitted for brevity
 9            });
10        return(false);
11    }
12    ,showMovieInfo: function(jsonData, requestStatus, xhrObject) {
13        // center a floater 1/2 as wide and 1/4 as tall as screen
14        let oneFourth = Math.ceil($(window).width() / 4);
15        $('#movieInfo').
16        css({'left': oneFourth,  'width': 2*oneFourth, 'top': 250}).
17        html($('<p>' + jsonData.description + '</p>'),
18                $('<a id="closeLink" href="#"></a>')).
19        show();
20        // make the Close link in the hidden element work
21        $('#closeLink').click(MoviePopupJson.hideMovieInfo);
22        return(false);  // prevent default link action
23    }
24    // hideMovieInfo omitted for brevity
25};
Figure 6.26: This version of MoviePopup expects a JSON rather than HTML response (line 5), so the success function uses the returned JSON data structure to create new HTML elements inside the popup div (lines 17–19; observe that jQuery DOM-manipulation functions such as append can take multiple arguments of distinct pieces of HTML to create). The functions omitted for brevity are the same as in Figure 6.14.

render :json works by calling to_json on object to create the string to send back to the client. The default implementation of to_json can serialize simple ActiveRecord objects, as Figure 6.25 shows.

To make an AJAX call that expects a JSON-encoded response, we just ensure that the argument object passed to $.ajax includes a dataType property whose value is the string json, as Figure 6.26 shows. The presence of this property tells jQuery to automatically call JSON.parse on the returned data, so you don’t have to do so yourself.

How can we test this code without calling the server every time? Happily, Jasmine- jQuery’s fixture mechanism allows us to specify JSON fixtures as well as HTML fixtures, as Figure 6.27 shows.

 1describe('MoviePopupJson', function() {
 2    describe('successful AJAX call', function() {
 3        beforeEach(function() {
 4        loadFixtures('movie_row.html');
 5        let jsonResponse = getJSONFixture('movie_info.json');
 6        spyOn($, 'ajax').and.callFake(function(ajaxArgs) {
 7            ajaxArgs.success(jsonResponse, '200');
 8        });
 9        $('#movies a').trigger('click');
10        });
11        // 'it' clauses are same as in movie_popup_spec.js
12    });
13});
Figure 6.27: Jasmine-jQuery expects to find fixture files containing .json data in spec/javascripts/fixtures/json. After executing line 5, jsonResponse will contain the actual JavaScript object (not the raw JSON string!) that will get passed to the success handler.
6.28
Figure 6.28: Architecture of in-browser SPAs that retrieve assets from multiple distinct services. Left: If the JavaScript code was served from RottenPotatoes.com, the default same-origin policy that browsers implement for JavaScript will forbid the code from making AJAX calls to servers in other domains. The cross-origin resource sharing (CORS) specification relaxes this restriction but is only supported by very recent browsers. Right: in the traditional SPA architecture, a single server serves the JavaScript code and interacts with other remote services. This arrangement respects the same-origin policy and also allows the main server to do additional work on behalf of the client if needed.

Self-Check 6.10.1. In Figure 6.27 showing the use of a JSON fixture, why do we also still need the HTML fixture to be loaded in line 4?

Line 9 tries to trigger the click handler for an element matching #movies a, and if we don’t load the HTML fixture representing a row of the movies table, no such element will exist. (Indeed, the MoviePopupJson.setup function tries to bind a click handler on this element, so that would also fail.) This is an example of using both an HTML fixture to simulate the user clicking on a page element and a JSON fixture to simulate a successful response from the server in response to that click.