Adding jQuery and AJAX features to a Rails site

Posted by TylerTaylor on January 13, 2017

In my Rails app, FilmSpot, a user can browse through movies and give them a rating, thus keeping track of which movies they’ve seen. The problem was that each step of the process required an entire page reload, and this caused the UI/UX to feel quite clunky.

  • Step 1 - User finds a movie
  • Step 2 - User clicks something along the lines of “Yes, I’ve seen this movie”
  • Step 3 - The whole site gets reloaded, now with a small form, allowing the user to pick from 1-5 stars
  • Step 4 - Once the form is submitted, the movie show page gets reloaded again, showing the user their rating

This was annoying for users (read: me!) and I knew something needed to be changed.

AJAX to the rescue!

Let’s walk through those 4 steps and see how easy it is to convert this to an AJAX call.

We’ve found our movie. Now we click “Yes, I’ve seen this movie”. This link goes to something like /users/5/viewings/new?movie_id=36 which tells me it’s trying to create a new viewing.

So, in the ViewingsController, our link hits the new action. We don’t want this to render the whole site again with a new form, we want this to load only the form. This can save us a lot of unnecessary loading time.

def new
  @viewing = Viewing.new
			
  render 'viewings/new', layout: false
end

This will return only the form data in viewings/new, without all the extra fluff.

But wait, this just loads an ugly form with no layout! Oh noes!

Here’s where AJAX comes in. In a nutshell, AJAX is the method of exchanging data with a server, and updating parts of a web page - without reloading the entire page.

We have a file called viewings.js. In this file, we want to listen for a click event on our “Yes” button.

function clickedYes() {
  $("a.clicked-yes").on("click", function(e) {
    // what do we need in here?!
  }
}

First and foremost, we want to prevent the “Yes” link from reloading the page and displaying just the bare form. To do this, we need to prevent the default behavior of the event. We pass in the event to our function, as e.

function clickedYes() {
  $("a.clicked-yes").on("click", function(e) {
    e.preventDefault();
				
    // great.. now what?!
  }
}

Now we need to make an AJAX request. We’ll use jQuery’s .get API to do this.

function clickedYes() {
  $("a.clicked-yes").on("click", function(e) {
    e.preventDefault();
				
    $.get(this.href).success(function(response){ // target this link's href, get a response
      $(".viewing").html("").append(response); // append response to the appropriate div
    })
  }
}

In this .get request, when successful, we get a response. This response is simply our viewings/new form! All I’m doing here is appending the response to a div with a class of .viewing.

So, just like that, we’ve already removed one page-reload. You click “Yes”, and bam, your rating form is almost immediately there, ready to use.

Now, this newly added form has a submit button with a class of rating-js. The problem is, this link was generated by AJAX after the DOM had already loaded, so our JS doesn’t know about it! To get around this, we’ll use what’s called event delegation. Event delegation allows us to add events to things that don’t exist yet, but may exist in the future. This just means we need to pick an element that is guaranteed to be present at the time the delegated event handler is attached. In this case, I’m going to use document.

This time, it’s going to be a POST request, so we need to specify that in the AJAX call.

function newRating() {
  $(document).on("click", ".rating-js", function(e) {
    e.preventDefault();
		
    $.ajax({
      method: "POST",
      url: e.target.form.action,
      data: $(e.target.form).serialize(),
      dataType: "JSON"
    }).success(function(response){
      // we need to do something with the response data here
    })
  })
}

Now, when we click “Rate this movie”, it will send the appropriate form data (including authenticity_token) to the ViewingsController create action.

We’ll tell it to render just the JSON for our new viewing instance.

def create
  #creating a new viewing, etc
	
  respond_to do |format|
    format.json { render json: @viewing }
  end
end

Then, in the AJAX callback, our response data will be a JSON object representing an instance of a viewing, like so:

Object {id: 123, user_id: 5, movie_id: 33, rating: 4, created_at: "2017-01-13"}

I created some functions to format and show the newly created rating, which also get called in the AJAX callback function.

$.ajax({
  method: "POST",
  url: e.target.form.action,
  data: $(e.target.form).serialize(),
  dataType: "JSON"
}).success(function(response){
  // creating a JS object
  let newViewing = new Viewing(response.rating)
  let addRating = newViewing.showRating()
  $('.viewing').html("").append(addRating)
})

Now - without ever reloading the website - a user can add and view their movie rating!

This is such a small example of such powerful features. I’ll try to write more about it as I learn about it.

Happy coding!