Using AJAX with Rails

Thus far, we've focused on Rails as a server-side architecture, meaning every server request happens synchronously, with a full page refresh.

Even in server-side architecture, we can still use AJAX for any action where we don't want a full page refresh. A good use case for this is "favoriting" a post on a blog, similar to "liking" a post on Facebook.

It would be a pretty bad experience for the user if we had to refresh the page every time we favorited a post, so we'll use AJAX to asynchronously request the server and jQuery to dynamically update the view with new information.

Model Relationships

Image we're starting with a 1:N relationship between users and posts. A user has_many posts, and a post belongs_to a user. This relationship represents the posts that a user creates.

#
# app/models/user.rb
#
class User < ActiveRecord::Base
  has_many :posts, dependent: :destroy
end
#
# app/models/post.rb
#
class Post < ActiveRecord::Base
  belongs_to :user
end

To implement favorites, we need to add a N:N relationship between users and posts. The favorites table will be our JOIN table, with foreign keys for user_id and post_id.

Here is what the updated models would look like for User and Post, and the new model for Favorite:

#
# app/models/user.rb
#
class User < ActiveRecord::Base
  has_many :posts, dependent: :destroy

  # `has_many through` relationship for favorites
  has_many :favorites, dependent: :destroy
  has_many :favorite_posts, through: :favorites, source: :post
end

Note: :favorite_posts serves as an alias for the posts associated with a user through :favorites. We can't say a user has_many :posts, because that relationship already exists, representing the posts that a user created. :favorite_posts represents the posts that a user favorited. We use the source attribute to indicate that :favorite_posts are records from the posts table in our database.

#
# app/models/post.rb
#
class Post < ActiveRecord::Base
  belongs_to :user

  # `has_many through` relationship for favorites
  has_many :favorites, dependent: :destroy
  has_many :users, through: :favorites
end
#
# app/models/favorite.rb
#
class Favorite < ActiveRecord::Base
  belongs_to :user
  belongs_to :post
end

Favorites Migration

After setting up the relationships between users, posts, and favorites, we need to add foreign keys to the favorites migration and run $ rake db:migrate.

#
# db/migrate/20150817033548_create_favorites.rb
#
class CreateFavorites < ActiveRecord::Migration
  def change
    create_table :favorites do |t|
      t.belongs_to :user
      t.belongs_to :post
      t.timestamps
    end
  end
end

Route to Create Favorites

Now that we set up our models and migrated our database, it's time to set up a new route to create favorites.

#
# config/routes.rb
#
Rails.application.routes.draw do
  resources :favorites, only: [:create]
end

Favorites Controller

In our FavoritesController, we'll define a method for create. This doesn't look too different from what we've done so far - we're just rendering json instead of rendering a view or redirecting.

Note that in the error case (when @favorite doesn't save), we want to send over an HTTP error status, which will trigger a failure response in our AJAX call.

#
# app/controllers/favorites_controller.rb
#
class FavoritesController < ApplicationController

  def create
    @favorite = current_user.favorites.new(favorite_params)
    if @favorite.save
      render json: @favorite
    else
      render json: { errors: @favorite.errors.full_messages }, status: :unprocessable_entity
    end
  end

  private
    def favorite_params
      params.require(:favorite).permit(:post_id)
    end

end

In the posts#index view, we'll put a link to favorite each post. We're using Bootstrap's glyphicon library to show a filled-in star if the user has already favorited the post and an empty star if the user has not favorited the post.

We put the class .favorite on our link, since the next step is to add a click event with jQuery, and we need to be able to select the element. We're also hiding <%= post.id %> in the link's data-id attribute, since we'll need a way to know which post the user clicked "favorite" for.

<!--
app/views/posts/index.html.erb
-->
<% @posts.each do |post| %>
  <h4><%= post.title %></h4>
  <p><%= post.content %></p>
  <p>
    <!-- show favorite icon if user is logged in -->
    <% if current_user %>
      <a href="javascript:void(0)" class="favorite" data-id="<%= post.id %>">

        <!-- check if user has already favorited this post to show correct star icon -->
        <% if current_user.favorite_posts.include? post %>
          <span class="glyphicon glyphicon-star"></span>
        <% else %>
          <span class="glyphicon glyphicon-star-empty"></span>
        <% end %>
      </a>
    <% end %>

    <span class="favorite-count"><%= post.users.count %></span> favorites
  </p>
<% end %>

AJAX Call to Create Favorite

As mentioned above, we're using the class .favorite to add a click event to the "favorite" link. The code below fills in the star when the user clicks "favorite" and sends a POST request to the server via AJAX to create the new favorite in the database. Once the response comes back from our AJAX call, we update the favorite count in the view.

//
// app/assets/javascripts/favorites.js
//
$(function() {

  $('.favorite').on('click', function() {
    // set variables
    var $favoriteIcon = $(this).find('.glyphicon');
    var $favoriteCount = $(this).next('.favorite-count');
    var count = parseInt($favoriteCount.text());

    // fill in star before ajax call for perceived speed
    $favoriteIcon.removeClass('glyphicon-star-empty').addClass('glyphicon-star');

    // ajax call to create new favorite in db
    $.post('/favorites', {
        favorite: { post_id: $(this).attr('data-id') }
      },
      function(data) {
        console.log(data);
        // update favorite count
        $favoriteCount.text(count + 1);
      }
    )
  });

});

Refactor Favorites Controller

Right now, our code allows a user to favorite a post as many times as they want. This doesn't seem like a very good experience, so we should check to see if a user has already favorited a post before creating a new favorite.

#
# app/controllers/favorites_controller.rb
#
class FavoritesController < ApplicationController

  def create
    post = Post.find_by_id(favorite_params[:post_id])

    # check if current_user has already favorited this post
    if current_user.favorite_posts.include? post
      render json: {}, status: :bad_request

    else
      @favorite = current_user.favorites.new(favorite_params)
      if @favorite.save
        render json: @favorite
      else
        render json: { errors: @favorite.errors.full_messages }, status: :unprocessable_entity
      end
    end
  end

  ...

end

Docs & Resources