The ability to like or favorite posts is very common on websites. This guide will show you how to add this functionality to your Rails 5 application without adding unnecessary bloat from gems. This guide assumes that you have already implemented posts (or similar) and users to your application and will not cover these steps.
Let's go over some design considerations before we begin. While everyone can view a post, only authenticated users can like posts by clicking a link or icon. We're keeping it positive by just tracking likes.
We're going to start by creating a model that represents the association between users and the posts they like. This will be a join table called 'Like'. If you need, you can use a different name and update the views and controllers to match.
rails g model Like user_id:integer post_id:integer
rake db:migrate
We're going to define the rout using nested resources like so:
resources :posts do
resource :like
end
resources :posts do
resource :like, module: :posts
end
We can easily created the needed folders in the terminal with:
mkdir app/controllers/posts
mkdir app/views/posts/likes
class Posts::LikesController < ApplicationController
before_action :authenticate_user!def create
@post.likes.where(user_id: current_user.id).first_or_createrespond_to do |format|
format.html { redirect_to @post }
format.js
end
endprivate
def set_post
@post = Post.find(params[:post_id])
end
end
<% if user_signed_in? && current_user.likes?(@post) %>
<%= link_to "Unlike", post_like_path(@post), method: :delete %>
<% else %>
<%= link_to "Like", post_like_path(@post), method: :post %>
<% end %>
Let's start a reference the partial that we created earlier by adding the following to app/views/posts/show.html.erb:
<div id="likes">
<%= render partial: "likes" %>
</div>Next we're going to update our User model to use the likes?() method as follows:
# User.rb
has_many :likesdef likes?(post)
post.likes.where(user_id: id).any?
end
def destroy
@post.likes.where(user_id: current_user.id).destroy_allrespond_to do |format|
format.html { redirect_to @post }
format.js
end
end
With this implemented, we've created the ability to like and unlike, but we're not rendering a Javascript response. This is actually pretty simple as we've already laid most of the groundwork. We already created a div with an id of 'likes' in app/views/posts/show.html.erb to help with this. First, lets add remote:true options to the likes partial:
<% if user_signed_in? && current_user.likes?(@post) %>
<%= link_to "Unlike", post_like_path(@post), method: :delete, remote: true %>
<% else %>
<%= link_to "Like", post_like_path(@post), method: :post, remote: true %>
<% end %>
app/views/posts/likes/create.js.erb
$("#likes").html("<%= j render partial: 'posts/likes' %>");
app/views/posts/likes/destroy.js.erb
$("#likes").html("<%= j render partial: 'posts/likes' %>");
We could add liking functionality to the post index by adding:
app/views/posts/index.html.erb
<p><%= render partial: 'likes' %></p>
This will give us an error because the @post variable is not set. We need to edit our partial and the code referencing the partials to work with the locals option.
app/views/posts/index.html.erb
<%= render partial: 'likes', locals: { post: post } %>
Let's also update the references in app/views/posts/show.html.erb
<div id="likes">
<%= render partial: 'likes', locals: { post: @post } %>
</div>
app/views/posts/likes/create.js.erb and app/views/posts/likes/destroy.js.erb
$("#likes").html("<%= j render partial: 'posts/likes', locals: {post: @post } %>");
app/views/posts/_likes.html.erb
<% if user_signed_in? && current_user.likes?(post) %>
<%= link_to "Unlike", post_like_path(post), method: :delete, remote: true %>
<% else %>
<%= link_to "Like", post_like_path(post), method: :post, remote: true %>
<% end %><%= post.likes.count %>
<% post.likes.each do |like| %>
<%= image_tag like.user.avatar_url, width: 20 %>
<% end %>
Now we should be able to view likes on the posts index, but if you click like it won't display correctly. We need to go into our responses and change them to work with the divs to get the post id.
app/views/posts/likes/create.js.erb and app/views/posts/likes/destroy.js.erb
$("#post_<%= @post.id %>_likes").html("<%= j render partial: 'posts/likes', locals: {post: @post } %>");
app/views/posts/index.html.erb
<p id="post_<%= post.id %>_likes"><%= render partial: 'likes' %></p>
<div id="post_<%= @post.id %>_likes">
<%= render partial: 'likes', locals: { post: @post } %>
</div>
Things you can do to extend this app: