CNK's blog

Rails Ajax Options

Rails and JavaScript: JQuery, JSON, Oh My!

With Rails ‘respond_to’ method, it is really easy to return differnt kinds of data from the same controller method based on the url extension. I was really happy with this when I was building a a site to provide a read-only version of an event calendar I had built years earlier on the ArsDigita Community System. To get data out of that Oracle database, I built a Rails web site that let users ask for events by category, sponsor, or lecture series with a variety of date restrictions. The data was consumed by several different clients - two who wanted XML and several others who wanted JSON. I could use exactly the same URLs by just adding .xml or .json to the url. Even better, I could have controller render an html version of the same data which developers could use to preview the result sets. Once they had all the parameters the way they wanted, they just copied the url and added ‘.json’ ahd used it in their web pages.

But now I actually want to move into the 21st century and build some web pages with AJAX interactions. Rails has supported a variety of methods of working with Javascript starting with the RJS methods in the version 1.x days. Currently Rails (4.2.0) uses JQuery by default - including a jquery-ujs package which, when combined with the ‘remote: true’ argument to ‘form_for’, makes it simple to have your forms submit via AJAX instead of a normal server request.

First, the regular form_for helper:

# This rails helper
<%= form_for(@course) do |f| %>

# Produces this html
<form class="new_course" id="new_course" action="/courses" accept-charset="UTF-8" method="post">
  <input name="utf8" type="hidden" value="✓">
  <input type="hidden" name="authenticity_token" value="YE006c08mOnIYENufqJJZVrV3yAo1sHOgk9EJ9ZqigLIUktT6Vg0Px8FMx5GTl54ewIKYOaWqJYpWgiPEpQDZQ==">

But with ‘remote: true’ (note no authenticity_token):

# This rails helper
<%= form_for(@course, remote: true) do |f| %>

# Produces this html
<form class="new_course" id="new_course" action="/courses" accept-charset="UTF-8" data-remote="true" method="post">
  <input name="utf8" type="hidden" value="✓">

This comes into the server and is interpretted as a JS request:

Started POST "/courses" for <ip> at 2015-03-12 23:07:10 -0700
Processing by Empcms4Tutors::CoursesController#create as JS

which will fall into the format.js case in the normal respond_to stanza, which by default, will try to render the create.js.erb template in your courses views. This view should jquery directives that will be executed in the browser.

$('#all_courses').append("<%= escape_javascript(render partial: 'table_row', locals: { course: @course }) %>");
$('#course_name').val(''); // clear form input field
$('#error_explanation').html(''); // clear error message

This flow has some pros and cons. On the bright side, it is relatively easy (as long as you remember to use ‘escape_javascript’ on any html you produce. But it means you have a fair amount of javascript scattered small files in your views directory. It is JS that manipulates your html so in that sense it is a view. But even if I am less than enthusiastic about that aspect, the big selling point is that I can reuse the code in my view partials. The exact same embedded ruby (erb) that created the table rows that loaded when the page first loaded is the erb that creates the new row JQuery inserts when I finish adding a new course.

Speaking JSON

If you want to keep all of your JavaScript together, for example if you are using a more extensive front end framework, then you may want to have your AJAX interactions communicate as JSON. The adjustments you need to make for that start with adding a data-type argument to your form tag:

# This rails helper
<%= form_for(@course, remote: true, format: json) do |f| %>

# Produces this html
<form class="new_course" id="new_course" data-type="json" action="/courses" accept-charset="UTF-8" data-remote="true" method="post">
  <input name="utf8" type="hidden" value="✓">

# Which shows up as this on the server
Processing by Empcms4Tutors::CoursesController#create as JSON

If you don’t add a different format section, this will also end up in the format.js section and will send back the jquery selectors and manipulations as before - with a Content-Type header of ‘text/javascript’. However, since the browser wasn’t expecting to get JavaScript, it doesn’t evaluate it (at least Chrome doesn’t). So I can see the server responding without error, and the data coming into the browser with a status code of 200. No JavaScript errors register in the console - but nothing happens on the page. All in all my least favorite type of error - no error, just nothing happens.

So to correct that, we add a ‘format.json’ section to our ‘respond_to’ block:

format.json { render json: @course }

So now the browser gets back a JSON representation of the new item with a Content-Type header of ‘application/json’. Then it is up to us to write the client-side JavaScript - for example to add the table row with the new element. That might look something like this (untested code):

$('#all_courses).on('ajax:success', function(event, data, status, xhr) {
                                    // add the table row with the course name, links, etc.
                }.on('ajax:error', function(event, xhr, status,error) {
                                   // insert the failure message near the form
                                   $('form').append(xhr.responseText)
                });

This keeps all your JavaScript on the client side - but at the expense of mixing some html into it as you build the new table row.

Comments