Liking posts functionality from scratch in Rails 5

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.

Creating the Likes model

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

If the user has a record for a post in the Likes table, then the user likes the post.  If no record exists, the user either does not like the post or has not voted yet.



Don't forget to run

rake db:migrate

Create the routes for likes

We're going to define the rout using nested resources like so:



resources :posts do
resource :like
end

As you can see, we have added a singular resource. This will allow the user to only have one like on each post. This construction also works best with how we are designing our controller as you will see.



Typically, we would create a LikesController in the controller folder, but we're actually going to scope the controllers and routes under posts.


resources :posts do
resource :like, module: :posts
end

Now if you check your routes, you'll see that likes are scoped under a posts folder.

Adding the files and folders

We can easily created the needed folders in the terminal with: 


mkdir app/controllers/posts

and


mkdir app/views/posts/likes

Now we can create the LikesController at app/controllers/posts/likes_controller.rb


class Posts::LikesController < ApplicationController
before_action :authenticate_user!

def create
@post.likes.where(user_id: current_user.id).first_or_create

respond_to do |format|
format.html { redirect_to @post }
format.js
end
end

private
def set_post
@post = Post.find(params[:post_id])
end
end


Let's go over this code. In the create action we're looking up the likesfor the post based on the current_user's id. Then we use the first_or_create method to get the first record or create a new one. This will prevent us from duplicates.



Next we created a respond_to block and send the HTML requests to be redirected to the posts and creates a JS response to render the template instead of redirecting back.



Now it's time to create the file that makes our solution work, app/views/posts/_likes.html.erb:



<% 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 %>

In this snippet, we check that the user is signed in and that the user likes the post. If this is the case, we display an 'unlike' link which deletes the record. If the user isn't signed in or has not liked the post, a 'like' link is displayed.

Implementing the solution

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 :likes

def likes?(post)
post.likes.where(user_id: id).any?
end


At this point, you should be able to like a post and then see the 'unlike' link. However, you should receive an error because we haven't created a 'destroy' action in our LikesController. Let's implement that:



def destroy
@post.likes.where(user_id: current_user.id).destroy_all

respond_to do |format|
format.html { redirect_to @post }
format.js
end
end


This is very similar to the create action but uses destroy_all instead of first-or_create. 

Rendering a Javascript response

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 %>

Now the app expects a Javascript response and will render that response on the page instead of reloading. Let's create the responses that will be needed.

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' %>");

Refining the code

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 } %>");

We need to go back to add the id to our index and show views.

 

app/views/posts/index.html.erb



<p id="post_<%= post.id %>_likes"><%= render partial: 'likes' %></p>

post/show.html.erb


<div id="post_<%= @post.id %>_likes">
<%= render partial: 'likes', locals: { post: @post } %>
</div>


Now the _likes partial is more flexible and tied to the appropriate posts.

Taking it further

Things you can do to extend this app:


  • Display number of likes

  • Display avatars of users who have liked

  • Track dislikes 

guide
Recently, while creating a subscription checkout form using Stripe Elements, I wanted to list the plans along wth their pricing and details. The form uses a collection input to list the plans as radio buttons, but the methods in this guide should work for checkboxes and selects.  Here is the input statement that we are working with for starters: <%= f.input :plan_id,   collection: Plan.published, as: :radio_buttons %> Currently, this will display the following label and radio buttons (Note that this label is provided using I18n): Please select your plan: ○ plan 1 ○ plan 2 I already have text with instructions...more
guide
For use with trusted/cleaned data. Add to application.rb def truncate_and_link(text,options ={}) length = options[:length] return text if length.blank? url = options[:url] || '#' output = raw text.truncate_words(length) output += link_to('more', url) if text.size > length output.html_safe end Adapted from: http://itiansrock.blogspot.com/2013/01/truncate-and-link-text-helper-in-rails.htmlmore
guide
Assuming that you have Rails and PostgreSQL already installed on your workstation, follow the steps for a new or existing Ruby on Rails app according to your needs. more
guide
Basic steps to create a new app using Rails API with a React frontend, then deploy on Heroku.
guide
Font Awesome 5 SVG icons use javascript to find <i> tags with an icon class and replaces them with an <svg> tag. Turbolinks online displays these icons on initial page load and/or refresh, however when following a link, Turbolinks reloads the page and not the icons. This guide assumes that you have already loaded font awesome into your project using your preferred method. For what its worth, this guide uses the font awesome CDN more
guide
Strings can be split using three built-in functions: Split() is a method that splits a String object into an array Reverse() is a method that reverses the array Join() is a method that joins an array into a string function reverseString(str) { return str.split("").reverse().join(""); } reverseString("hello"); more