Hearting, Liking, or Starring with Rails

Blueprint Ruby on Rails

Last updated Feb 16, 2016

Hearting, Liking, Starring, Pinning, Poking, Following, Voting, Flagging, Favoriting - call them what you will - are web application features that all follow the same general pattern. They use an intermediate database table to keep track of a relationship between 2 other tables. For example, if a “User can star a post”, the the star table is the middleman that stores a user_id and a post_id, thus creating the relationship.

Since we have to choose a specific feature, we’re going to go with Hearts. In this lesson, we will use the has_many :through ActiveRecord relationship to allow Users to Heart Posts. We will also use AJAX to allow Users to Heart! and Unheart! posts seamlessly. For quicker development, Devise will be used to implement User authentication.

Gemfile

gem 'devise'

Lesson Checkpoints

1. Generating the Initial Models

First, we’ll generate the 3 initial models needed to implement the Hearting feature. At this point, I am assuming you have devise installed in your app, if not follow these instructions. This lesson can be easily adapted to other user authentication strategies as well.

console

$ rails g devise User

$ rails g scaffold Post title:string

$ rails g resource Heart

db/migrate/timestamp_create_hearts.rb

The migration file for the Hearts table will add a user_id and post_id column to the Hearts table. After you edit the migration, run rake db:migrate.

def change
  create_table :hearts do |t|
    t.belongs_to :post, index: true
    t.belongs_to :user, index: true
    t.timestamps null: false
  end
end

Define Model Relationships

Now we just need to define the ActiveRecord relationships.

models/heart.rb

class Heart < ActiveRecord::Base
  belongs_to :post

belongs_to :user

end

models/user.rb

class User < ActiveRecord::Base
  has_many :hearts, dependent: :destroy
  has_many :posts, through: :hearts
end

models/post.rb

class Post < ActiveRecord::Base
  has_many :hearts, dependent: :destroy
  has_many :users, through: :hearts
end

2. DRY Methods to Heart Posts

At this point, it will be useful to have a couple methods to create and destroy rows in the heart table. We will use these methods to keep our controller and view code DRY.

models/user.rb

# creates a new heart row with post_id and user_id
def heart!(post)
  self.hearts.create!(post_id: post.id)
end

# destroys a heart with matching post_id and user_id
def unheart!(post)
  heart = self.hearts.find_by_post_id(post.id)
  heart.destroy!
end

# returns true of false if a post is hearted by user
def heart?(post)
  self.hearts.find_by_post_id(post.id)
end

3. Prevent Duplicate Relationships with Scoped Validation

It is technically possible for Users to heart the same post more then once. We can prevent this in the Heart model with a validation that is scoped to a specific post.

models/heart.rb

validates :user_id, uniqueness: { scope: :post_id }

4. Custom Controller Actions to Heart and Unheart

We are going to rendering JavaScript templates for these actions, so we use the respond_to :js line.

controllers/hearts_controller.rb

respond_to :js

def heart
  @user = current_user
  @post = Post.find(params[:post_id])
  @user.heart!(@post)
end

def unheart
  @user = current_user
  @heart = @user.hearts.find_by_post_id(params[:post_id])
  @post = Post.find(params[:post_id])
  @heart.destroy!
end

5. Custom Routes to Heart and Unheart

Even though we’ll be rendering these actions with AJAX, we need to define a couple routes that match our controller actions.

config/routes.rb

match 'heart', to: 'hearts#heart', via: :post

match 'unheart', to: 'hearts#unheart', via: :delete

6. Creating a Unique Div for each Button

In this case, we are going to display a feed of posts. Our JavaScript will need to update a specific Heart button, so we need to give each one a unique id. The div_for helper make this easy by wrapping content with a <div id='post_id'>

views/posts/index.html.erb

<% @posts.each do |post| %>
  <% post.title %>
  <%= div_for post do %>
     <%= render partial: "hearts/button”, locals: { post: post } %>
  <% end %>
<% end %>

7. Create a Button Partial

Everything is now in place for Users to Heart Posts, we just need to make it possible in the front-end views. We will start by creating a partial that will conditionally display either a Heart or Unheart button for the User.

views/hearts/_button.html.erb

<% if current_user.heart?(post) %>
  <%= button_to "Unheart", unheart_path(post_id: post.id), remote: true, method: :delete  %>
<% else %>
  <%= button_to "Heart", heart_path(post_id: post.id), remote: true  %>
<% end %>

8. Update the Page with jQuery

Our last step is to create js.erb templates that will be rendered when the User clicks the Heart/Unheart Button. Note: The 'div#<%= dom_id(@post) %>' is referencing the div_for helper method we used in step 6.

views/hearts/heart.js.erb

$('div#<%= dom_id(@post) %>').html('<%= j render partial: "button", locals: {post: @post} %>');

views/hearts/unheart.js.erb

$('div#<%= dom_id(@post) %>').html('<%= j render partial: "button", locals: {post: @post} %>');

That’s it. Now go apply this same basic process to Liking, Starring, Pinning, Poking, Following, Voting, and Flagging! As a finishing touch, try adding a two different CSS classes to the button to represent the heart and unheart states.

Comments