The Rails 4 Way (2014)
Chapter 19. Ajax on Rails
Ajax isn’t a technology. It’s really several technologies, each flourishing in its own right, coming together in powerful new ways
—Jesse J. Garrett, who coined the name AJAX
Ajax is an acronym that stands for Asynchronous JavaScript and XML. It encompasses techniques that allow us to liven up web pages with behaviors that happen outside the normal HTTP request life-cycle (without a page refresh).
Some example use-cases for Ajax techniques are:
· “Type ahead” input suggestion, as in Google search.
· Sending form data asynchronously.
· Seamless navigation of web-presented maps, as in Google Maps.
· Dynamically updated lists and tables, as in Gmail and other web-based email services.
· Web-based spreadsheets.
· Forms that allow in-place editing.
· Live preview of formatted writing alongside a text input.
Ajax is made possible by the XMLHttpRequestObject (or XHR for short), an API that is available in all modern browsers. It allows JavaScript code on the browser to exchange data with the server and use it to change the user interface of your application on the fly, without needing a page refresh. Working directly with XHR in a cross-browser-compatible way is difficult, to say the least, however we are lucky as the open-source ecosystem flourishes with Ajax JavaScript libraries.
Incidentally, Ajax, especially in Rails, has very little to do with XML, despite its presence there at the end of the acronym. In fact, by default Rails 4 does not include XML parsing (however, this can be reenabled). The payload of those asynchronous requests going back and forth to the server can be anything. Often it’s just a matter of form parameters posted to the server, and receiving snippets of HTML back, for dynamic insertion into the page’s DOM. Many times it even makes sense for the server to send back data encoded in a simple kind of JavaScript called JavaScript Object Notation (JSON).
It’s outside the scope of this book to teach you the fundamentals of JavaScript and/or Ajax. It’s also outside of our scope to dive into the design considerations of adding Ajax to your application, elements of which are lengthy and occasionally controversial. Proper coverage of those subjects would require a whole book and there are many such books to choose from in the marketplace. Therefore, the rest of the chapter will assume that you understand what Ajax is and why you would use it in your applications. It also assumes that you have a basic understanding of JavaScript programming.
19.0.1 Firebug
Firebug106 is an extremely powerful extension for Firefox and a must-have tool for doing Ajax work. It lets you inspect Ajax requests and probe the DOM of the page extensively, even letting you change elements and CSS styles on the fly and see the results on your browser screen. It also has a very powerful JavaScript debugger that you can use to set watch expressions and breakpoints.
Firebug also has an interactive console, which allows you to experiment with JavaScript in the browser just as you would use irb in Ruby. In some cases, the code samples in this chapter are copied from the Firebug console, which has a >>> prompt.
As I’ve jokingly told many of my Ruby on Rails students when covering Ajax on Rails: “Even if you don’t listen to anything else I say, use Firebug! The productivity gains you experience will make up for my fee very quickly.”
Kevin says… Alternatively, if you use Chrome or Safari, both browsers have similar built-in tools. My personal preference is the Chrome DevTools107, which is continously improved with each new release of Chrome. |
19.1 Unobtrusive JavaScript
The Unobtrusive JavaScript (UJS) features in Rails provide a library-independent API for specifying Ajax actions. The Rails team has provided UJS implementations for both jQuery and Prototype, available under https://github.com/rails/jquery-ujs and https://github.com/rails/prototype-rails, respectively. By default, newly-generated Rails applications use jQuery as its JavaScript library of choice.
To integrate jQuery into your Rails application, simply include the jquery-rails gem in your Gemfile and run bundle install. Next, ensure that the right directives are present in your JavaScript manifest file (listed below).
1 # Gemfile
2 gem 'jquery-rails'
1 // app/assets/javascripts/application.js
2 //= require jquery
3 //= require jquery_ujs
By including those require statements in your JavaScript manifest file, both the jQuery and jquery_ujs libraries will automatically be bundled up along with the rest of your assets and served to the browser efficiently. Use of manifest files is covered in detail in Chapter 20, “Asset Pipeline”.
19.1.1 UJS Usage
Prior to version 3.0, Rails was not unobstrusive, resulting in generated markup being coupled to your JavaScript library of choice. For example, one of the most dramatic changes caused by the move to UJS was the way that delete links were generated.
1 = link_to 'Delete', user_path(1), method: :delete,
2 data: { confirm: "Are you sure?" }
Prior to the use of UJS techniques, the resulting HTML would look something like
1 <a href="/users/1" onclick="if (confirm('Sure?')) { var f =
2 document.createElement('form'); f.style.display = 'none';
3 this.parentNode.appendChild(f); f.method = 'POST'; f.action =
4 this.href;var m = document.createElement('input'); m.setAttribute('type',
5 'hidden'); m.setAttribute('name', '_method'); m.setAttribute('value',
6 'delete'); f.appendChild(m);f.submit(); };return false;">Delete</a>
Now, taking advantage of UJS, it will look like
1 <a data-confirm="Are you sure?" data-method="delete" href="/users/1"
2 rel="nofollow">Delete</a>
Note that Rails uses the standard HTML5 data- attributes method as a means to attach custom events to DOM elements.
Also required for Rails UJS support is the csrf_meta_tag, which must be placed in the head of the document and adds the csrf-param and csrf-token meta tags used in dynamic form generation.
1 %head
2 = csrf_meta_tag
CSRF stands for cross-site request forgery and the csrf_meta_tag is one method of helping to prevent the attack from happening. CSRF is covered in detail in Chapter 15, “Security”.
19.1.2 Helpers
As covered in Chapter 11, “All About Helpers”, Rails ships with view helper methods to generate markup for common HTML elements. The following is a listing of Action View helpers that have hooks to enable Ajax behavior via the Unobtrusive JavaScript driver.
19.1.2.1 button_to
The button_to helper generates a form containing a single button that submits to the URL created by the set of options. Setting the :remote option to true, allows the unobtrusive JavaScript driver to make an Ajax request in the background to the URL.
To illustrate, the following markup
= button_to("New User", new_user_path, remote: true)
generates
1 <form action="/users/new" class="button_to" data-remote="true"
2 method="post">
3 <div>
4 <input type="submit" value="New User">
5 <input name="authenticity_token" type="hidden"
6 value="HDVQ/5AHK+f5ChqN8qaah8Pd0gZzkoa21vqbvbayHBY=">
7 </div>
8 </form>
To display a JavaScript confirmation prompt with a question specified, supply data attribute :confirm with a question. If accepted, the button will be submitted normally; otherwise, no action is taken.
= button_to("Deactivate", user, data: { confirm: 'Are you sure?' })
The Unobtrusive JavaScript driver also allows for the disabling of the button when clicked via the :disable_with data attribute. This prevents duplicate requests from hitting the server from subsequent button clicks by a user. If used in combination with remote: true, once the request is complete, the Unobtrusive JavaScript driver will re-enable the button and reset the text to its original value.
1 = button_to("Deactivate", user, data: { disable_with: 'Deactivating...' })
19.1.2.2 form_for
The form_for helper is used to create forms with an Active Model instance. To enable the submission of a form via Ajax, set the :remote option to true. For instance, assuming we had a form to create a new user, the following:
1 = form_for(user, remote: true) do |f|
2 ...
would generate
1 <form accept-charset="UTF-8" action="/users" class="new_user"
2 data-remote="true" id="new_user" method="post">
3 ...
4 </form>
19.1.2.3 form_tag
Like form_for, the form_tag accepts the :remote option to allow for Ajax form submission. For detailed information on form_tag, see Chapter 11, “All About Helpers”.
19.1.2.4 link_to
The link_to helper creates a link tag of the given name using a URL created by the set of options. Setting the option :remote to true, allows the unobtrusive JavaScript driver to make an Ajax request to the URL instead of the following the link.
= link_to "User", user, remote: true
By default, all links will always perform an HTTP GET request. To specify an an alternative HTTP verb, such as DELETE, one can set the :method option with the desired HTTP verb (:post, :patch, or :delete).
= link_to "Delete User", user, method: :delete
If the user has JavaScript disabled, the request will always fall back to using GET, no matter what :method you have specified.
The link_to helper also accepts data attributes :confirm and :disable_with, covered earlier in the button_to section.
19.1.3 jQuery UJS Custom Events
When a form, link, or button is marked with the data-remote attribute, the jQuery UJS driver fires the following custom events:
Event name |
parameters |
Occurrence |
ajax:before |
event |
Ajax event is started, |
aborts if stopped. |
||
ajax:beforeSend |
event, xhr, |
Before request is sent, |
settings |
aborts if stopped. |
|
ajax:send |
event, xhr |
Request is sent. |
ajax:success |
event, data, |
Request completed and HTTP |
status, xhr |
response was a success. |
|
ajax:error |
event, xhr, |
Request completed and HTTP |
status, error |
response returned an error. |
|
ajax:complete |
event, xhr, |
After request completed, |
status |
regardless of outcome. |
|
ajax:aborted:required |
event, elements |
When there exists blank |
required field in a form. |
||
Continues with submission if |
||
stopped. |
||
ajax:aborted:file |
event, elements |
When there exists a populated |
file field in the form. |
||
Aborts if stopped. |
This allows you, for instance, to handle the success/failure of Ajax submissions. To illustrate, let’s bind to both the ajax:success and ajax:error events in the following CoffeeScript:
1 $(document).ready ->
2 $("#new_user")
3 .on "ajax:success", (event, data, status, xhr) ->
4 $(@).append xhr.responseText
5 .on "ajax:error", (event, xhr, status, error) ->
6 $(@).append "Something bad happened"
19.2 Turbolinks
Rails 4 introduces a new, controversial, feature called Turbolinks. Turbolinks is JavaScript library, that when enabled, attaches a click handler to all links of a HTML page. When a link is clicked, Turbolinks will execute an Ajax request, and replace the contents of the current page with the response’s <body> tag.
Using Turbolinks also changes the address of the current page, allowing users to bookmark a specific page and use the back button as they normally would. Turbolinks uses the HTML5 history API to achieve this.
The biggest advantage of Turbolinks is that it enables the user’s browser to only fetch the required stylesheets, javascripts, and even images once to render the page. Turbolinks effectively makes your site appear faster and more responsive.
To integrate Turbolinks into your existing Rails application, simply include the turbolinks gem in your Gemfile and run bundle install. Next, add “require turbolinks” in your JavaScript manifest file.
1 # Gemfile
2 gem 'turbolinks'
1 // app/assets/javascripts/application.js
2 //= require jquery
3 //= require jquery_ujs
4 //= require turbolinks
19.2.1 Turbolinks usage
In Rails 4, Turbolinks is enabled by default, but can be disabled if you prefer not to use it. To disable the use of Turbolinks for a specific link on a page, simply use the data-no-turbolink tag like so:
= link_to 'User', user_path(1), 'data-no-turbolink' => true
It does not depend on any particular framework, such as jQuery or ZeptoJS, and is intended on being as unobtrusive as possible.
One caveat to Turbolinks is it only will work with GET requests. You can, however send POST requests to a Turbolink-enabled link, as long as it sends a redirect instead of an immediate render. This is because the method must return the user’s browser to a location that can be rendered on a GET request (pushState does not record the HTTP method, only the path per request).
19.2.2 Turbolink Events
When using Turbolinks, the DOM’s ready event will only be fired on the initial page request, as it overrides the normal page loading process. This means you cannot rely on DOMContentLoaded or jQuery.ready() to trigger code evaluation. To trigger code that is dependent on the loading of a page in Turbolinks, one must attach to the custom Turbolinks page:change event.
1 $(document).on "page:change", ->
2 alert "loaded!"
When Turbolinks requests a fresh version of a page from the server, the following events are fired on document:
page:before-change
A link that is Turbolinks-enabled has been clicked. Returning false will cancel the Turbolinks process.
page:fetch
Turbolinks has started fetching a new target page.
page:receive
The new target page has been fetched from the server.
page:change
The page has been parsed and changed to the new version.
page:update
If jQuery is included, triggered on jQuery’s ajaxSucess event.
page:load
End of page loading process.
By default, Turbolinks caches 10 page loads to reduce requests to the server. In this case, the page:restore event is fired at the end of the restore process.
jquery.turbolinks If you have an existing Rails application that extensively binds to the jQuery.ready event, you may want to look at using the jquery.turbolinks library108. When Turbolinks triggers the page:load event on a document, jquery.turbolinks will automatically jQuery.ready events as well. |
19.2.3 Controversy
Turbolinks undoubtedly speeds up many sites by avoiding reprocessing of the <head> tag. It was mature enough for the Rails core team to bundle it as an official part of Rails. Yet it has plenty of critics. Some raise objections about the headaches of making sure that all the Ajax functions of a large application actually work correctly with Turbolinks enabled. Others point out how it breaks apps in older browsers such as IE8. And others point out that it is inefficient, because most applications could get away with refreshing sections of the page smaller than the entire <body> element.
We think it’s worth giving Turbolinks a try in your application, especially if you’re starting from scratch and can take its challenges into account from the beginning of a project. However, we also admit that we’ve disabled it in a lot of our own projects. Your mileage may vary.
Here are some of the issues that you may need to address with your use of Turbolinks:109
Memory leaks
Turbolinks does not clear or reload your JavaScript when the page changes. You could potentially see the effects of memory leaks in your applications, especially if you use a lot of JavaScript.
Event Bindings
You have to take older browsers into consideration. Make sure you listen for page:* events, as well as DOMContentLoaded.
Client-side frameworks
Turbolinks may not play nicely with other client-side frameworks like Backbone, Angular, Knockout, Ember, etc.
19.3 Ajax and JSON
JavaScript Object Notation (JSON) is a simple way to encode JavaScript objects. It is also considered a language-independent data format, making it a compact, human-readable, and versatile interchange format. This is the preferred method of interchanging data between the web application code running on the server and any code running in the browser, particularly for Ajax requests.
Rails provides a to_json on every object, using a sensible mechanism to do so for every type. For example, BigDecimal objects, although numbers, are serialized to JSON as strings, since that is the best way to represent a BigDecimal in a language-independent manner. You can always customize the to_json method of any of your classes if you wish, but it should not be necessary to do so.
19.3.1 Ajax link_to
To illustrate an Ajax request, let’s enable our Client controller to respond to JSON and provide a method to supply the number of draft timesheets outstanding for each client:
1 respond_to :html, :xml, :json
2 ...
3 # GET /clients/counts
4 # GET /clients/counts.json
5 def counts
6 respond_with(Client.all_with_counts) do |format|
7 format.html { redirect_to clients_path }
8 end
9 end
This uses the Client class method all_with_counts which returns an array of hashmaps:
1 defself.all_with_counts
2 all.map do |client|
3 { id: client.id, draft_timesheets_count: client.timesheets.draft.count }
4 end
5 end
When GET /clients/counts is requested and the content type is JSON the response is:
1 [{"draft_timesheets_count":0, "id":20},
2 {"draft_timesheets_count":1, "id":21}]
You will note in the code example that HTML and XML are also supported content types for the response, so it’s up to the client to decide which format works best for them. We’ll look at formats other than JSON in the next few sections.
In this case, our Client index view requests a response in JSON format:
1 - content_for :head do
2 = javascript_include_tag 'clients.js'
3 ...
4 %table#clients_list
5 ...
6 - @clients.each do |client|
7 %tr[client]
8 %td= client.name
9 %td= client.code
10 %td.draft_timesheets_count= client.timesheets.draft.count
11 ...
12 = link_to 'Update draft timesheets count', counts_clients_path,
13 remote: true, data: { type: :json }, id: 'update_draft_timesheets'
To complete the asynchronous part of this Ajax-enabled feature, we also need to add an event-handler to the UJS ajax:success event, fired when the Ajax call on the update_draft_timesheets element completes successfully. Here, jQuery is used to bind a JavaScript function to the event once the page has loaded. This is defined in clients.js:
1 $(function() {
2 $("#update_draft_timesheets").on("ajax:success", function(event, data) {
3 $(data).each(function() {
4 var td = $('#client_' + this.id + ' .draft_timesheets_count')
5 td.html(this.draft_timesheets_count);
6 });
7 });
8 });
In each row of the clients listing, the respective td with a class of draft_timesheets_count is updated in place with the values from the JSON response. There is no need for a page refresh and user experience is improved.
As an architectural constraint, this does require this snippet of JavaScript to have intimate knowledge of the target page’s HTML structure and how to transform the JSON into changes on the DOM. This is a major reason why JSON is the best format for decoupling the presentation layer of your application or, more importantly, when the page is requesting JSON from another application altogether.
Sometimes, however, it may be desirable for the server to respond with a snippet of HTML which is used to replace a region of the target page.
19.4 Ajax and HTML
The Ruby classes in your Rails application will normally contain the bulk of that application’s logic and state. Ajax-heavy applications can leverage that logic and state by transferring HTML, rather than JSON, to manipulate the DOM.
A web application may respond to an Ajax request with an HTML fragment, used to insert or replace an existing part of the page. This is most usually done when the transformation relies on complex business rules and perhaps complex state that would be inefficient to duplicate in JavaScript.
Let’s say your application needs to display clients in some sort of priority order, and that order is highly variable and dependent on the current context. There could be a swag of rules dictating what order they are shown in. Perhaps it’s that whenever a client has more than a number of draft timesheets, we want to flag that in the page.
1 %td.draft_timesheets_count
2 - if client.timesheets.draft.count > 3
3 %span.drafts-overlimit WARNING!
4 %br
5 = client.timesheets.draft.count
Along with that, let’s say on a Friday or Saturday we need to group clients by their hottest spending day so we can make ourselves an action plan for the beginning of the following week.
These are just two business rules that, when combined, are a bit of a handful to implement both in Rails and in JavaScript. Applications tend to have many more than just two rules combining and it quickly becomes prohibitive to implement those rules in JavaScript to transform JSON into DOM changes. That’s particularly true when the page making the Ajax call is external and not one we’ve written.
We can opt to transfer HTML in the Ajax call and using JavaScript to update a section of the page with that HTML. Under one context, the snippet of HTML returned could look like
1 <tr id="client_22" class="client"></tr>
2 <tr>
3 <td></td><td>Aardworkers</td><td>AARD</td><td>$4321</td>
4 <td class="draft_timesheets_count">0</td>
5 </tr>
6 <tr id="client_23" class="client"></tr>
7 <tr>
8 <td></td><td>Zorganization</td><td>ZORG</td><td>$9999</td>
9 <td class="draft_timesheets_count">1</td>
10 </tr>
Whereas, in another context, it could look like
1 <tr>
2 <td>Friday</td>
3 </tr>
4 <tr>
5 <td>Saturday</td>
6 </tr>
7 <tr id="client_24" class="client"></tr>
8 <tr>
9 <td></td><td>Hashrocket</td><td>HR</td><td>$12000</td>
10 <td class="draft_timesheets_count">
11 <span class="drafts-overlimit">WARNING!</span>
12 5
13 </td>
14 </tr>
15 <tr id="client_22" class="client"></tr>
16 <tr>
17 <td></td><td>Aardworkers</td><td>AARD</td><td>$4321</td>
18 <td class="draft_timesheets_count">0</td>
19 </tr>
The JavaScript event handler for the Ajax response then just needs to update the innerHTML of a particular HTML element to alter the page, without having to know anything about the business rules used to determine what the resulting HTML should be.
19.5 Ajax and JavaScript
The primary reason you want to work with a JavaScript response to an Ajax request is when it is for JSONP (JSON with Padding). JSONP pads, or wraps, JSON data in a call to a JavaScript function that exists on your page. You specify the name of that function in a callback query string parameter. Note that some public APIs may use something other than callback, but it has become the convention in Rails and most JSONP applications.
Xavier says…
Although the Wikipedia entry110 for Ajax does not specifically mention JSONP and the request is not XHR by Rails’ definition, we’d like to think of it as Ajax anyways - it is after all asynchronous JavaScript.
JSONP is one technique for obtaining cross-domain data, avoiding the browser’s same-origin policy. This introduces a pile of safety and security issues that are beyond the scope of this book. However, if you need to use JSONP, the Rails stack provides an easy way to handle JSONP requests (with Rack::JSONP) or make JSONP requests (with UJS and jQuery).
To respond to JSONP requests, activate the Rack JSONP module from the rack-contrib RubyGem in your environment.rb file:
1 classApplication < Rails::Application
2 require 'rack/contrib'
3 config.middleware.use 'Rack::JSONP'
4 ...
then, just use UJS to tell jQuery it’s a JSONP call by altering the data-type to jsonp:
1 = link_to 'Update draft timesheets count', counts_clients_path,
2 remote: true, data: { type: :jsonp }, id: 'update_draft_timesheets'
jQuery automatically adds the ?callback= and random function name to the query string of the request URI. In addition to this it also adds the necessary script tags to our document to bypass the same-origin policy. Our existing event handler is bound to ajax:success so it is called with the data just like before. Now, though, it can receive that data from another web application.
jQuery also makes the request as if it is for JavaScript, so our Rails controller needs to respond_to :js. Unfortunately, the Rails automatic rendering for JavaScript responses isn’t there yet so we add a special handler for JavaScript in our controller:
1 respond_to :html, :js
2 ...
3
4 def counts
5 respond_with(Client.all_with_counts) do |format|
6 format.html { redirect_to clients_path }
7 format.js { render json: Client.all_with_counts.to_json }
8 end
9 end
We still convert our data to JSON. The Rack::JSONP module then pads that JSON data in a call to the JavaScript function specified in the query string of the request. The response looks like this:
jsonp123456789([{"id":1,"draft_timesheets_count":0},
{"id":2,"draft_timesheets_count":1}])
When the Ajax response is complete, your Ajax event handler is called and the JSON data is passed to it as a parameter.
19.6 Conclusion
The success of Rails is often correlated to the rise of Web 2.0, and one of the factors linking Rails into that phenomenon is its baked-in support for Ajax. There are a ton of books about Ajax programming, as it’s a big subject, but an important enough part of Rails that we felt the need to include a quick introduction to it as part of this book.