0

I'm trying to build a form for a like button. This like model is polymorphic to different types of Models (comments / posts / etc.) and belongs to a certain user.

When this user is viewing a blog item for instance, I want to show a like button below the post. I've set-up my routes in a way that the like routes are always nested inside the polymorphic object for which they are destined:

So for posts for instance:

#routes.rb
resources :posts do
   resources :likes, only: [:create, :destroy]
end

So a post link would look like /posts/:post_id/likes/ (method: Post)

In the controller I create a new Like object, assign it to a user and save it. This works perfectly.

The problem is when I'm trying to create delete form. I really have no clue how to create that, I know the link should be /posts/:post_id/like/:id (method: Delete), but configuring it this way results in an error.

I think the form could also be refactored, but I have no clue on how making forms for these kind of 'complex' relationships.

#shared/_like_button.html.haml

- if not @post.is_liked_by current_user
  = form_for(@post.likes.build, url: post_likes_path(@post)) do |f|
  = f.submit
- else
  = form_for(@post.likes.find_by(user_id: current_user.id), url: post_like_path(@post), html: {method: :delete}) do |f|
= f.submit

I think the main problem is that post_like_path(@post) doesn't get rendered correctly because I'm not aware of the :id of the like. So I keep getting an ActionController::UrlGenerationError in PostsController#show error when trying to build the link.

bo-oz
  • 2,842
  • 2
  • 24
  • 44

2 Answers2

2

This should work:

= form_for([@post, @post.likes.find_by(user_id: current_user.id)], html: {method: :delete}) do |f|

url: post_like_path(@post) in your code expects a second argument (the like object). That's what throws the error. But you don't need it actualy if you put the nested resources as an array in the first argument of the form_for helper.

If the record passed to form_for is a resource, i.e. it corresponds to a set of RESTful routes, e.g. defined using the resources method in config/routes.rb. In this case Rails will simply infer the appropriate URL from the record itself. (source: http://apidock.com/rails/ActionView/Helpers/FormHelper/form_for)

You can pass an array of resources if your resource is nested within another resource.

Now... You probably want to re-use this code for your other polymorphic models as well. You can do this by passing @post, or @comment to your partial like this:

= render :partial => 'like_button', locals: {likable: @post}

and refactor your partial like this:

= form_for([likable, likable.likes.find_by(user_id: current_user.id)], html: { method: :delete}) do |form|
Sander Garretsen
  • 1,683
  • 10
  • 19
  • Great thanks! Could you explain what the multiple variables in the brackets [@post, @post.likes] are meaning? – bo-oz Oct 02 '15 at 13:49
  • When you nest routes, you need to pass all the objects to the url helper that have an :id in the route itself, so you are passing an array of all the objects defined in the route - for the create action it only needs a post (/posts/:id/likes), but for the delete it needs both a post and a like object (/posts/:post_id/likes/:id) – RichardAE Oct 02 '15 at 13:54
  • Great, thanks! One last question, after that partials I need to include some more partials (and even a .erb rendered partial from a ajax callback) and keep setting the same variable. Can I set the variable as a global is the first partial? @likeable = likeable? – bo-oz Oct 02 '15 at 14:50
1

There's no need to use an actual form, you could do it with a link_to. Here's an example with basic text link (to make sure it's working)

- if not @post.is_liked_by current_user
  = link_to 'Like', post_like_path(@post), method: :post
- else
  = link_to 'Delete', post_like_path([@post, @post.likes.find_by(user_id: current_user.id)]), method: :delete

Then you use an image/button as the link itself.

- if not @post.is_liked_by current_user
  = link_to post_like_path(@post), method: :post do
    # some html image/button
- else
  = link_to post_like_path([@post, @post.likes.find_by(user_id: current_user.id)]), method: :delete do
    # some html image/button

Updated this code with help from the accepted answer so anyone looking in future can use link_to.

RichardAE
  • 2,945
  • 1
  • 17
  • 20
  • I've already tried something like this, problem is that I keep getting a: ActionController::UrlGenerationError in PostsController#show error. Although the link shows up in my routes. Probably because the like :id is not being set! (and I don't need to, because I have all the information to delete the like) – bo-oz Oct 02 '15 at 13:45
  • Updated the answer with Sander's URL fix. – RichardAE Oct 02 '15 at 13:51