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:
How do we get the server app to generate JSON in response to AJAX requests, rather than rendering HTML view templates or partials?
How does the client specify that it expects a JSON response, and how does it use the JSON response data to modify the DOM?
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};
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});
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.