I am trying to accomplish what I think to be the most direct, though certainly not the most elegant, replication of a reddit style vote button. I have code that works without the AJAX, but I would like users to be able to click the vote buttons and they change appropriately without reloading the page (Hence the AJAX).
The problem occurs when I try to remove the vote button clicked on and replace with the appropriate 'highlighted' vote button. The reason I am replaceing the vote button is because I need to change it to a button that will unvote if clicked on.
(I have simplified this to just handle just upvoting since downvoting is the same mexhanic). Here is my view:
<% @posts.each do |post| %>
<%= post.content %>
<% if current_user.voted_as_when_voted_for post %>
<%= render 'upvoted', post: post %>
<% else %>
<%= render 'upvote', post: post %>
<% end %>
<% end %>
The voted_as_when_voted_for method returns true if they have upvoted the post. Here is the upvote and upvoted forms:
<%= link_to image_tag('upvoted.png', size: '18'), remove_like_post_path(post), method: :put, remote: true, id: "upvoted#{post.id}" %>
<%= link_to image_tag('upvote.png', size: '18'), like_post_path(post), method: :put, remote: true, id: "upvote#{post.id}" %>
Here is my controller:
def upvote
@post = Post.find(params[:id])
@post.liked_by current_user
respond_to do |format|
format.html { redirect_to @post.parent }
format.js {}
end
end
def remove_upvote
@post = Post.find(params[:id])
@post.unliked_by current_user
respond_to do |format|
format.html { redirect_to @post.parent }
format.js {}
end
end
upvote.js.erb: and upvoted.js.erb:
$('#upvote<%= @post.id %>').remove().append('<%= j render("upvoted") %>');
$('#upvoted<%= @post.id %>').remove().append('<%= j render("upvote") %>');
With those files I get the following console errors:
PUT http://localhost:3000/posts/96/like 500 (Internal Server Error)
NameError in Posts#upvote
undefined local variable or method `post' for #<#<Class:0x007fde1836cbf0>:0x007fde18c00af0>
_upvote.html.erb and _upvoted.html.erb
<%= link_to image_tag('upvote.png', size: '18'), like_post_path(post), method: :put, remote: true, id: "upvote#{post.id}" %>
<%= link_to image_tag('upvoted.png', size: '18'), like_post_path(post), method: :put, remote: true, id: "upvoted#{post.id}" %>
I also tried something like this based on the link below:
$('#upvote<%= @post.id %>').remove().after('<%= j render "upvoted", post: @post %>');
When I use that code, the upvote is succesfully removed, but the upvoted form is not added on. So it seems that I need to use the instance variable, but something is still not quite working.
What am I missing??
This person seemed to have a similar problem: Rails Ajax Render Partial
DRY
Doesn't look very DRY to me
You could remove a lot of the duplicate code by making better use of your HTTP Verbs - by defining a delete
route (for downvote) & post
route (for upvote):
#config/routes.rb
resources :posts do
match :vote, via: [:post, :delete] #-> domain.com/posts/14/vote
end
#app/controllers/posts_controller.rb
Class PostsController < ActiveRecord::Base
respond_to :js, :html
def vote
@post = Post.find params[:id]
if request.post?
@post.liked_by current_user
elsif request.delete?
@post.unliked_by current_user
end
respond_with @post
end
end
Views
This will allow you to create the following JS & views:
#app/views/posts/vote.js.erb
$("<%= 'upvote##{@post.id}' %>").html('<%= j render "vote", locals: { post: @post } %>');
-
#app/views/posts/_vote.html.erb
image = current_user.voted_as_when_voted_for(post) ? "downvote.png" : "upvote.png"
method = current_user.voted_as_when_voted_for(post) ? :delete : :post
<%= link_to image_tag(image, size: '18'), post_vote_path(post), method: method, remote: true, id: "upvote#{post.id}" %>
-
#app/views/posts/show.html.erb
<% @posts.each do |post| %>
<%= post.content %>
<%= render 'vote', post: post %>
<% end %>
Fix
After looking at your code, I would look at the error for hints as to what the issue might be:
undefined local variable or method `post'
The problem seems to be called in your upvote
action, which will have the partial
upvoted
like this:
$('#upvoted<%= @post.id %>').remove().append('<%= j render("upvote") %>');
The problem is this partial requires the post
local variable. So the issue is that you're not passing the @post
to your partial
via the locals
argument
My code above addresses this, but in order to resolve it, you should just use locals: { post: @post }
OK, so the real culprit to the ajax failing was using .remove().after
My theory is that if I remove the element, then you can't add something after. So this fix solved the problem:
$('#upvote<%= @post.id %>').after('<%= j render "upvoted", post: @post %>');
$('#upvote<%= @post.id %>').remove();
Maybe not the most sophisticated, but it works! Note: I didn't want to use .hide().after()
because I didn't want lots of hidden links lurking behind.