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.