Skip to main content

Join GoRails to continue learning

Subscribe to GoRails to get access to this episode and all other pro episodes, and new awesome content every month.

Subscribe Now
Only $19/month

Login to your account

131 Liking Posts

Episode 24 · September 15, 2014

Add liking or favoriting to your app like Facebook or Twitter

ActiveRecord


Transcripts

Subscribe or login to view the transcript for this episode.

Discussion


Gravatar

Learned a lot of new things through this post, thank you. Never knew how to properly write a route to a module or the proper architecture for that set up. Very useful stuff. Would have liked to see you use div_for and dom_id methods when it comes to assigning the ids to the p tags and rendering out JS at the end. I feel like both of those are under utilized functions that handle a lot of the hard coded id values, etc...

Gravatar

It's like you know what's going through my mind. I'm making a dedicated screencast for div_for. ;-)

Gravatar

RIght on! I'm excited to hear your insight on it. It's a practice I only started using a few weeks ago, and you seem to have a way of explaining things I already know in a way that helps me understand them on a deeper level. Thank you again :)


Gravatar

Great screencast! I'm behind on my rails development because of a really good Ruby course I'm in, but I will try this out soon. Thank you so much for these!


Gravatar

Hey! I really appreciate all these screencasts, happily pro user. For this I'm getting an "undefined method 'likes'" on the user model so i'm a little stuck. Any ideas why?

Cheers! :)

Gravatar

Well, i restarted the server several times and somehow it worked :)

Gravatar

Haha! It's possible there was something accidentally getting cached, but it's hard to say.

Gravatar

:P Wouldn't be the first time magic happens on server restart. One thing that didn't work for me at the end is redirection to login page when a user is not logged in. I'm getting the "POST /unauthenticated HTTP/1.1" 401 in the server, but no actual redirection. Any idea why? (do you have a repo for this course?)

Cheers from Ecuador!


Gravatar

Thank you for these screencasts! They are awesome. I keep getting an error "Routing Error uninitialized constant Posts" when I click the 'like' link. I feel like this is a fairly simple fix? Any help would be much appreciated. Thanks in advance.

Gravatar

If it's in reference to your Post database model (That's my guess) then you want to reference singular "Post" and not plural "Posts" in your controller. Pluralization can get you easily with Rails. :)

Gravatar

Thank you for responding! I got past the uninitialized constant error now I keep getting 'undefined method likes'. I am trying to set up this like button on a nested resource (a lesson inside of a course), would that change things? Thanks again

Gravatar

Not really. The only thing that will change is querying for the nested object and adding likes to it.

Your controller would do something like:

@course = Course.find(params[:course_id])
@lesson = @course.lessons.find(params[:id])

And then as long as you have your likes association on the Lesson model, you should be good to go.

Gravatar

I didn't have has_many: likes in my Lesson model! Thank you! I also had to add @course = Course.find(params[:course_id]) in my def create and def create part of my Lesson::LikesController for proper routing. It works beautifully. Thanks again for these.

Gravatar

Glad you got it working! :)

Gravatar

It works great. Random question, but is there a way to sort the posts on the index page by most liked?

Gravatar

Sure can. You could use the counter_cache option to save the count to the model and then you can order by that method.

Gravatar

Excellent. Thank you


Gravatar

Chris, very useful screencast.

I can get it to work well with just rails, but when I implement ajax I get the following error.

"ActionView::Template::Error (undefined method `likes' for nil:NilClass):"

I've customized it a bit by making polymorphic 'likeable' likes.

Gravatar

It sounds like your variable that you called ".likes" on was a nil. May need to double check that the find is grabbing the post (or whatever model) is returning the right object.


Gravatar

Thanks for that screencast. I even watched the forum series and the "polymorphic comments". I m wondering what we do if we want to have likes in our forum for threads and posts? Would you use the polymorphic associations?

Gravatar

Yep! You can make the likes polymorphic if you want them to apply to more than one model. That would let you add likes to Threads and Posts. You'd build it pretty much the same way and that should do the trick.


Gravatar

Chris,

Any idea what query I could put in my user model to find out all posts that have not (yet) been liked by the user?

Gravatar

The easiest way is to use a counter cache column. It can keep track of how many likes each post has and saves it on the post so that you can quickly and easily query that. There's an old Railscast on it that I'd recommend: http://railscasts.com/episo...

You could also do it with a join and a group by query, but a counter cache will let you sort the information at any time quickly.

Gravatar

Awesome, many thanks ;)


Gravatar

This is awesome! Just the functionality I need. Was looking at the "Act-as-votable-gem" but that seems to be overkill as I only need upvotes. Just need to figure out how the gem does the counter caching stuff now

https://github.com/ryanto/a...


Gravatar

Hey Chris,

How do I make the before_action :authenticate_user! to work with ajax? If works perfectly, when there is no remote: true and redirects to the login page if a user is not signed in. However, when using ajax, nothing happens other than a "401 unauthorized" in the logs.

Thanks in advance.

Gravatar

Yeah, that's an issue with redirects and AJAX. jquery_ujs tries to combat that by following the redirect, but it obviously doesn't help since you need to handle that response separately. The simplest solution and what you should generally do is to just disable those links when users are logged out.

If you want to keep them enabled, but then show a sign in form, it's quite a bit more complex and would require you to write your own handler in JS to capture that response and handle it separately.

Gravatar

Thank Chris. Yea I think I'll have to go with the second option. Still trying to master working if JS in rails though.


Gravatar

Hey Chris,

First of all, thank's for the quality of your video.

Unfortunately I'am stuck in the middle of the video, i'am facing with this error : undefined method `likes' for, it highlights

<% if user_signed_in? && current_user.likes?(@pin) %>

@pin is the name for post in my app.
def likes? is written in the User model, could you explain why ?

Cheers

Gravatar

According to the error, it sounds like maybe you don't have "has_many :likes" set up in your User model correctly so it can't find it. Double check that you've got that in there.

And "likes?" is written in the User model as a cleaner syntax for determining if the user likes a an object. Basically you wouldn't want to have the logic for determining that duplicated in your app everywhere, so we put it in a method in the model to make it more readable everywhere else.

Gravatar

Thk's for this very reactive answer, but i double checked on User Model, and i'v got the mention has_many :likes, if you have a another clue, i'am in, is that line correct : <% if user_signed_in? && current_user.likes?(@pin) %>
and this in my User Model :
has_many :likes
def likes?(pin)
pin.likes.where(user_id: id).any?
end

Gravatar

Whoops, actually that is going to call ".likes" on the "pin" variable, so you will need to double check "has_many :likes" on the Pin model. If that is there, then you potentially passed in a nil for pin. Then you'll need to check your controller to make sure that @pin got set to a Pin.


Gravatar

@excid3:disqus I have a likes partial inside of a parent directory like this: app/views/recipes/likes/_like.html.erb. I am trying to render inside the show action of the recipes controller and I am getting an error: missing partial: app/views/recipes/likes/_like. I'm not sure what is throwing the error.
likes partial below:

<% if @recipe.liked_by?(current_user) %>
<%= link_to "Unlike", recipe_like_path(@recipe), method: :post %>
<% else %>
<%= link_to "Like", recipe_like_path(@recipe), method: :post %>
<% end %>

In my Recipes::LikesController create action:

@like = Like.where(user_id: current_user.id, recipe_id: @recipe.id)
if @like.nil?
@like = @recipe.likes.where(user_id: current_user.id).first_or_create!
@recipe_was_liked = true
else
@like.destroy
@post_was_liked = false
end

Gravatar

whoops. I figured it out!


Gravatar

Been looking for a way to do this for a while, great episode...now how would I list all the all the posts that a user has liked? For example, I can do something like this:
<ul>
<%= current_user.owns(@book).each do |book| %>
<li><%= book.id.to_s %></li>
<% end %>
</ul>

...but alas this only shows me the own.id whereas I would like it to show me, for example the book.title. Also, this works with the show action, but in my index it doesn't as I have set up a <% @book.each do |title| %>. I can place the code inline, replacing @book with title but then my ajax button change is rendered useless. I'm fairly new to Rails so any ideas would be welcome!

Gravatar

You can set up an association to get the liked books through the likes the user has. It follows basically the same format. Remember that your book variable from the each is actually a full Book record so you can print out the id, the title, or whatever just by changing what you reference. Here's an example that would list out the book titles that a user likes. (I haven't tested this so it may not work right off the bat)

class User
has_many :likes
has_many :liked_books, through: :likes, class_name: "Book"
end

<%= current_user.liked_books.each do |book| %>
<li><%= book.title %></li>
<% end %>
Gravatar

Just what I needed, thanks Chris! I did go through the has_many documentation for rails and had to change class_name: "Book" to to source: :books though to avoid the to_sym error I was getting. Now I just need to figure out how to make the buttons load via ajax, that only works in the show action. Keep up the awesome work!

Gravatar

A follow-up question...the "like" button works in my show action as that does use @book, but when going through the for each statement above, it will not work as the variable is no longer @book but book. Is there a workaround to this?

Gravatar

So when you loop through each book, you need to reference the "book" local variable, not the "@book" instance variable when rendering the button. This is because the each yields the current book in the loop to the code inside the each block so it can run multiple times every book in the array.

Gravatar

That's what I did but it works when the code is inline in the view itself not when referenced as a partial.

Gravatar

You'll need to make sure you pass the book into the partial because they have different scopes for local variables.


Gravatar

getting an ActionController::RoutingError (uninitialized constant Pins) error in the logs. Can seem to figure it out

Routes are nested like so:

resources :pins do
resource :like, only: [:new,:create, :destroy], module: :pins
end

In Index View- this is pins/likes/like partial

<% if current_user.likes?(pin) %>
<%= link_to "", pin_like_path(pin), method: :delete, class: "fa fa-pinterest-p like", remote: true %>
<% else %>
<%= link_to "", pin_like_path(pin), method: :post, class: "fa fa-pinterest-p ", remote: true %>
<% end %>

Then I have this nested controller with create and destroy actions
class Pins::LikesController < ApplicationController
...
end

Gravatar

fixed it renamed folder from pins_controller to pins


Gravatar

My ajax isn't firing. I am getting an ActionView Template error, undefined local variable or method 'pin'

After I refresh the glyphicon changes color signifying it hit the db. In my index I have <%= render partial: 'pins/likes/like', locals: {pin: pin} %> and the guts of the partial is in my comment below. The partial sits in the block:
@pins.each do |pin|
...
end

Any ideas off the top of your head?

Gravatar

Should it be locals: {pin: @pin} instead? I'm assuming you are just missing sending the pin from the controller.

Gravatar

I was eating dinner and this popped into my head exactly! But thanks so much! You're really great Chris at responding to people's question's. Aside from the great content you release, your willingness to help will keep me coming back to GoRails for a long time - or as long as you keep it live!

Gravatar

Thanks! I appreciate that a lot! :)


Gravatar

I've been playing with joins but can't seem to find the answer: I can displayed a user's liked posts (current.user.liked_posts.each do) but they are sorted by the creation date of the post. How can I sort by the like.created_at date?

Gravatar

You can do a SQL ORDER on the created_cat column by adding liked_posts.order(created_at: :desc) or :asc if you want ascending.

Gravatar

That's what I'm doing...I have current_user.liked_posts.order(created_at: :desc) and it shows the correct posts liked by the user BUT they are ordered by the created_at date of the posts, not the likes.

Gravatar

Nevermind, problem solved. Instead of doing order(created_at: :desc), i did order("likes.created_at desc") and now they are sorted by the like date instead of the post date. Thanks!


Gravatar

Gravatar

Gravatar

Interested to see the source code!


Gravatar

@excid3:disqus Thanks for implementing with basic functionality. I have few questions:

1. On index pages to show all posts, it will have N+1 to check if the current_user like each post. Is there any good way to remove that?
2. I know we could eager load with `includes(:likes)` to solve N+1, just imagine if we have many likes for each post, and we just want to show the total count, and the last 3 likers either radom or from last, how do we do eager load them and prevent to load entire likes collection from db?

Gravatar

The best way is really to load up all the user's likes for those related objects in a single query. Then you can check those post against the likes result with Ruby and that will be 2 queries instead.

You'll have to do some custom SQL to pull off #2, but it's not so bad. Check this out: https://robots.thoughtbot.c...


Gravatar

@excid3:disqus I am not yet a paid member so I couldn't see this screencast yet, I just have one question though. Is this the kind of "LIKE" that the page will not refresh? Because I only knew acts_as_votable now but my problem is if I click the upvote button, the page will refresh.

Gravatar

Yep! It's all AJAX based and includes showing avatars and stuff if you wanted to show a few people's faces who liked the post as well.

Gravatar

Alright! Can't wait to learn this one 😀


Gravatar

Hi, Chris. I am using the friendly_id gem to make the profiles url.
On my like controller I have this def set_profile @profile = Profile.friendly.find(params[:id]) end.
Everything work great but when I click like button I get 404 erro message to console.
The link looks like this
Like and the console error is
POST jquery.self-a714331225d...... http://localhost:3000/profiles/vjandrei/like 404 (Not Found) What I miss here?

Gravatar

Ou I found the problem out :)

def set_profile
@profile = Profile.friendly.find(params[:profile_id])

Gravatar

Glad you got it fixed! 👍

Gravatar

But how about the images. I have in the profiles db image for each profile and try to get the image now like this.
<% profile.likes.each do |like| %>
<%= image_tag like.profile.image.url(), width: 20 %>
<% end %>

Now it will show the profile image that has been liked instead of the user image of the person who liked the profile.

Here is image, under the social media buttons there is liked button pressed and liked.


Gravatar

Thanks Chris, this episode is so helpful!


Gravatar

Hi Chris, I implemented this liking posts and it was working fine until I added Friendly_ids to the urls.
When adding the friendly_ids , the show action in Posts controller was changed from @post = Post.find(params[:id]) to @post = Post.find_by slug: params[:id]. I have tried replacing post.id under the likes_create/destroy to post.slug but it doesn't work.
Can you please advice on how to fix this?


Gravatar

Hi Chris,

I've got this working well without ajax, but as soon as I add 'remote: true' I get a 404 error in the console. The button is pressed, but doesn't update.

Any ideas as to why this is?


Gravatar

Hi Chris I applied this to a project I'm working on and managed to get it working as per the video, so many thanks. It helped me a great deal, still a newbie.
How would you go about creating a separate view to show all the objects you have liked.


Gravatar

I have this working as per the tutorial but wanted to have a link in my navbar to show all the likes by the user, i.e. my likes, has anyone implemented this tutorial in this way.

Gravatar

All you have to do is add a controller for it and then you can loop through current_user.likes.each do |like| and print them out on the page.

Gravatar

Thanks for the reply Chris, I'm still pretty new with the rails stuff, so wasn't able to get it working after creating a new controller probably something simple maybe I'll work it out. Might be a good follow up video though I know it's such a common thing on most sites to see Favs, Likes etc but there isn't a great deal of resources out there that show all the steps through to completion for newbies like me.


Gravatar

Hi Chris,
Why did you decide to roll your own liking system rather than using `acts_as_votable`?

Gravatar

Main reason is it's such a simple feature, no need to use a gem. Always great to have one less dependency, less likely to break on upgrading Rails versions, etc. And the benefit is I can customize it and use it exactly how I want.


Gravatar
Jeremy Christopher Bray

Hi @excid3:disqus loving gorails. Would be super helpful for newbies if all the screenshots were in the transcript. Was chasing my tail with a stupid relationship problem, until I went to the source files.


Gravatar

Hi Chris, just subscribed, really nice stuff here. Regarding this tutorial, I have a question about caching: how would you implement this like feature without having to use `current_user` as the cache key (which obviously would defeat the purpose of the cache in the first place) ? Would you recommend using a gem like `render_async` to do this?

Gravatar

Well, you can cache the user signed out version and use that everywhere. That will cover a lot of things if your website gets much traffic from search results. The other thing I'd say is to use an AJAX request or just embed the IDs in the HTML somewhere for the logged in users and just have JS update the cache'd snippet visually to reflect whether the user had voted on it.

This way you can use the same cache for literally everyone and then just tweak it visually based upon the current user with a little JS. That's how I'm handling the watchlist and completed states of videos on GoRails now so I can have a single cache for every episode and then update it visually for logged in users.


Gravatar

Hi Chris, it's been a while since you posted this, but I'm having an issue and can't figure it out. Basically when I click "Like" everything seems to be working fine and the counter goes up and the button changes to "unlike". But when I click "Unlike" nothing happens until I go to another page. Any idea why that might be? Thanks in advance!


Gravatar
Another great episode. I'm setting this up now, users can favorite audio tracks. My models are User, Track, and Favorite. I'm stuck on the part where my partial renders "unfavorite" or "favorite" depending on whether the association exists. 

<% if user_signed_in? && current_user.favorited?(@track) %>
  <%= link_to "Unavorite", track_unfavorite_path(@track), method: :post %>
<% else %>
  <%= link_to "Favorite", track_favorite_path(@track), method: :post %>
<% end %>

The problem is, if you click to favorite the track in the view, the new favorite gets created in the database, but the view still says "favorite" instead of "unfavorite." 

 => #<Favorite id: 2, user_id: 3, track_id: 6, created_at: "2018-06-21 15:47:33", updated_at: "2018-06-21 15:47:33">

Something seems to be wrong with the method in my user model. It's not recognizing that the record exists. 

  def favorited?(track)
    track.favorites.where(user_id: id).any?
  end

I have also tried

favorites.include?(track)

but that doesn't work either. No matter which version I try, my view doesn't recognize that there's a favorite already. 
Gravatar
Disregard. I wrote the code correctly here, but in my user.rb file I had it written as

 
 def favorited?(track)
    track.favorites.where(user_id: :id).any?
  end 



Gravatar

I have one more question about this. I have this working on the individual show page, but how can I properly render the button for an index of the posts? 

This is my partial at tracks/favorites 
<% if user_signed_in? && current_user.favorited?(@track) %>
<%= link_to image_tag('icons/heart_full.svg', width: 20), track_unfavorite_path(@track), method: :post, remote: true %>
<% else %>
<%= link_to image_tag('icons/heart_empty.svg', width: 20), track_favorite_path(@track), method: :post, remote: true %>
<% end %>

My js files are here 
$("#favorites").html("<%= escape_javascript(render('tracks/favorites')) %>");

The problem is when I try to render the partial onto an index. I am looping through all favorited items here as "item".  Everything works fine until I include the partial. 
<div id="favorites">
<%= render 'tracks/favorites', :track => item %>
</div>

When I use this, I get an error.  
undefined method `favorites' for nil:NilClass

It is telling me the problem is with this method. 

  def favorited?(track)

track.favorites.where(user_id: id).any?

end

Gravatar

Great Video. However I'm stuck and would love some help.
I have listings results incoming from an API. On my index view I want to add the like or heart button however when i add

<%= link_to 'Like', listing_like_index_path(@listing), method: :post %> it's just break and it's because I don't have :listing_id

these are my routes for like
resources :listings, only: [:index, :show] do
resources :like, module: :listings
end

I added resources instead of resource because after the click the button I want them to stay on the index view

Any help will be deeply apreciated

Gravatar

solved it by using

<%= link_to 'Like', listing_like_index_path(listing.mls_id), method: :post %>
dumb mistake on my part


Gravatar

Hi Chris, I've been followed you along for a while, (I just changed the account frequently because I change the workplace as well) and I really appreciate your work. I just wonder that do you share the beginning project of each video. Because I've been feeling kind of hard to follow along your video without the beginning project. Hope you can consider this. Thank you so much. Keep up the good work.

Gravatar

The base project is generally using https://github.com/excid3/jumpstart

Gravatar

Thank you very much!


Login or create an account to join the conversation.