Skip to content

Instantly share code, notes, and snippets.

@excid3
Created October 29, 2016 00:27
Show Gist options
  • Save excid3/5e0b9ce4c9bc149b534b95c0d6615bb9 to your computer and use it in GitHub Desktop.
Save excid3/5e0b9ce4c9bc149b534b95c0d6615bb9 to your computer and use it in GitHub Desktop.
GoRails Transcript 024 - Liking Posts

Liking Posts

A common feature of applications these days is the ability to like or favorite a post on websites.

Today we're going to talk about implementing that and submitting it via an AJAX request.

We're starting with a simple application that has posts and users. Anyone is able to create a blog post and click on them and view. But we want to add the ability for a user to click a link and Like the post.

The first step we need to take is to create a model to represent the association between the user and the post they liked.

We call this a join table. We're going to create a model and call it Like. Calling it a Like makes the most sense in this case because it is what the user will see, but if you need to you can use a different name and update the views and controllers accordingly.

rails g model Like user_id:integer post_id:integer

If the record exists that means the user likes that post, if it doesn't exist that means the user doesn't or haven't liked the post yet.

Because we just generated a model we'll need to remember to run rake db:migrate

So now we're going jump into the config/routes.rb file and create the route for our likes. But we're going to do it differently than a normal route.

  resources :posts do
    resource :like
  end

In this case we've added a singular resource :like to the block for the posts and that will allow only the current user to have only one like on each post.

You can see the effect this has if you rake routes | grep like in the Terminal and you'll see that you have most of the typical CRUD actions but you only have it for one.

The reason we're doing it this way is because of how we're designing our controller so let's work on the controller now.

When you look at the rake routes and you observe that the controller is likes you could go ahead and create a LikesController but one thing to consider is using a module on your routes to scope the controllers. Let's scope our like route to be under posts.

config/routes.rb

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

Now if you observe the rake routes | grep like results you'll see its scoped under a posts folder. This helps keep things nice and organized.

Let's create the folders we need. mkdir app/controllers/posts and mkdir app/views/posts/likes

Let's create the app/controllers/posts/likes_controller.rb

class Posts::LikesController < ApplicationController
 #code
end

We've created the controller and folders for the likes, but before we finish the controller let's go create the Like button on the app/views/posts/show.html.erb page.

<div id="likes">
  <%= render partial: "likes" %>
</div>

We've created a div and are using a partial to do the rendering of the likes so when we update it with Javascript later it is faster and simpler for us.

Let's go create that likes partial we just referenced. touch app/views/posts/_likes.html.erb

app/views/posts/_likes.html.erb

<%= link_to "Like", post_like_path(@post), method: :post %>

So now if you go to a post and click on a Like link you'll see an undefined action for the create and that's because we haven't created that.

So let's go back to the app/controllers/posts/likes_controller.rb

One thing you'll need to do is make sure that the user is logged in before it creates the like so we'll use a before_action devise provides to do so.

We also need to set the post so we'll create a private method of set_post and grab the post based off the params from the URL.

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

So let's break down the code we just wrote. In the create action we're looking up the likes for the post based off the current_user's id. Then we're getting the first record for it or creating it. By using the first_or_create method it will prevent us from getting duplicate posts.

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

Now let's go back to our likes partial and we see we have a likes link, but we need a method to show an Unlike link if you've already liked the post.

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

We check to make sure you're signed in and if you are signed in then we check to see if you've liked the post. If you have we display an Unlike link which we are submitting via a DELETE request to unlike the post if you desire.

But if you're paying attention we haven't created the current_user.likes? method so we need to do that.

app/models/user.rb

# User.rb
has_many :likes

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

So we've created the likes? method on the user which is basically the same code we used in the likes controller create action but here we use the any? method which will return true or false based on the results. Which is exactly what we want and need for our views.

Now if you check the posts view you'll see you can like a post and then if you check again now you can see an Unlike link on a post you liked.

We expect to receive an error about missing the destroy action because we haven't created that so lets jump back to the likes controller and create that.

app/controllers/posts/likes_controllers.rb

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

You'll notice it's basically the same code we have in our create action but instead of using first_or_create we're using destroy_all to remove the records of the like where our user_id matches for that post. We then respond in the same ways as we did with the create action.

Alright so we've created the liking and unliking functionality but we still have not displayed the avatars for the users and we're not actually rendering a javascript response. It's basically just reloading the page so fast we don't realize its not an AJAX(XHR) request.

To add the javascript responses it's going to be pretty simple. We created an div with an id of "likes" so we'll use that in the javascript.

We need to add the remote: true options to the likes partial.

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

Now the app will expect a javascript response and render that response on the page.

Let's create the responses now that we've created the links to be javascript triggered.

app/views/posts/likes/create.js.erb

($"#likes").html("<%= j render partial: 'posts/likes' %>");

We actually can copy that into the app/views/posts/likes/destroy.js.erb as well and it'll render the correct partials.

So now we have liking and unliking just like we'd see on Facebook or Twitter.

The last feature we'd like to implement is displaying avatars of users who have liked the post next to the Like button.

Because we don't actually have avatars built into the app we're going to build a quick gravatar implementation to add those for the other users.

app/models/user.rb

def avatar_url
  hash = Digest::MD5.hexdigest(email)
  "http://www.gravatar.com/avatar/#{hash}"
end

Basically the way Gravatar works is based on an email address's md5 hash. So we're creating the MD5 hash using Digest::MD5.hexdigest for the users email and then passing it into the url for Gravatar. If the email doesn't have a Gravatar already a default one will be given back.

So now if we open the likes partial again we can add the number of likes quickly by adding

<%= @post.likes.count %>

You'll see that it has a count of the likes for the posts next to it.

Now we can iterate through the likes and display the avatar for each user who liked it. Here's the finished 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 %>

<%= @post.likes.count %>

<% @post.likes.each do |like| %>
  <%= image_tag like.user.avatar_url, width: 20 %>
<% end %>

We now are displaying Gravatars for users who've liked the posts. If you like and unlike the post you'll see your avatar showing up and going away.

Before you go though one thing to think about is if we wanted to add the list of likes to the Posts index we could add that by doing...

app/views/posts/index.html.erb

<p><%= render partial: 'likes' %></p>

But you'll see that it gives us an error because the @post variable is not set so we'll want to edit the partial by adding the locals option to the partials.

<%= render partial: 'likes', locals: { post: post } %>

Now we just need to go update all of the references for this in the posts/show.html.erb

But for the posts show we need to pass in @post instead

app/views/posts/show.html.erb

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

And you'll need to do the same in the create.js.erb and destroy.js.erb those should look like

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

Now we just need to edit the likes partial again to remove all the @post symbols

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 it'll use the locals when rendering the partial.

So now we're displaying the likes on the posts#index but if you notice you can click like but it doesn't display correctly and thats because of the JS response for the likes we need to go into the app/views/posts/likes/create.js.erb and change it to look at the divs closer.

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

That will look for a div similar to post_1_likes, post_2_likes etc based on the ID of the post. We need to go back and edit the id of the p tag for the posts index view

app/views/posts/index.html.erb

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

One last thing is we'll also change the post/show.html.erb to be the same

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

So there you have it, now we're using the partial for all of the views and liking/unliking will work on the index and show views. If you have duplicates of the same post on a page you may run into other issues, but if you do you can change the javascript to a class selector.

Thanks for watching and reading!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment