Pt-F-5
Jon says:
Some of these solutions looked a little overly-complicated, so I wanted to post my pre-ajax solution as well:
In the controllerline_items_controller:
def destroy
@cart = current_cart
@line_item = @cart.remove_product(@cart.line_items.find(params[:id])) #the @cart is a security measure to ensure that the item is within the current cart; I'm not certain if this is needed
respond_to do |format|
format.html { redirect_to store_url }
format.xml { head :ok }
end
end
In the model cart.rb:
def remove_product(item)
if item.quantity > 1
item.quantity -= 1
item.save
item #returns the current item if a quantity remains
else
item.destroy
false #returns false if no items are left
end
end
...and finally in the view _line_item.html.erb:
<% if line_item == @current_item %>
<tr id="current_item">
<% else %>
<tr>
<% end %>
<td><%= line_item.quantity %>×</td>
<td><%= line_item.product.title %></td>
<td class="item_price"><%= number_to_currency(line_item.total_price) %></td>
<td><%= button_to 'Remove', line_item, :method => :delete %>
</tr>
Ethan says:
For the life of me I can’t get the unit test for this to work.
Here is my line_items_controller.rb destroy method:
def destroy
@cart = current_cart
if current_cart
@line_item = @cart.remove_product(params[:id])
else
@line_item = LineItem.find(params[:id])
@line_item.destroy
end
if current_cart.total_items < 1
current_cart.destroy
end if
respond_to do |format|
if current_cart.line_items.empty?
format.html {redirect_to(store_url, :notice=>'Your cart is empty')}
format.js {@current_item = @line_item}
else
format.html { redirect_to(store_url, :notice=>'Deleted from the cart') }
format.js {@current_item = @line_item}
end
format.xml { head :ok }
end
end
and the unit test:
test "should destroy line_item via ajax" do
assert_difference('LineItem.count', -1) do
xhr :delete, :destroy, :id => products(:ruby).id
end
assert_response :success
end
I just get:
1) Failure:
test_should_destroy_line_item_via_ajax(LineItemsControllerTest) [test/functional/line_items_controller_test.rb:44]:
"LineItem.count" didn't change by 1.
<3> expected but was
<2>.
Paul says:
Aleisha’s solution didn’t seem to work for me, and every other answer on this page seems to relate to older versions of Rails. Any help?
Aleisha says:
This is the solution to the problem that worked for me using Rails 3.0.3. It’s a brute force method so if anyone knows of anything else please let me know.
In _line_item.html.erb
<td><%= button_to ('-', line_item_path(line_item.product_id), :method => :delete, :confirm => 'Are you sure?') %></td>
In cart.rb
def deduct_product(product_id)
current_item = line_items.select {|i| i.product_id.to_i == product_id.to_i }.first
if current_item.quantity > 1
current_item.quantity -= 1
current_item.save
elsif current_item.quantity == 1
current_item.quantity -= 1
current_item.destroy
end
current_item
end
In line_items_controller.rb
# DELETE /line_items/1
# DELETE /line_items/1.xml
def destroy
@line_item = @cart.deduct_product(params[:id])
respond_to do |format|
if current_cart.line_items.empty?
format.html { redirect_to(store_url, :notice=> 'Your cart is empty') }
else
format.html { redirect_to(store_url, :notice=> 'Item Removed') }
end
format.xml { head :ok }
end
end
In order to get highlighting and disappearing to happen for deleted items I created a file called destroy.js.rjs and placed it in app/views/line_items/
page.replace_html("cart", render(@cart))
page[:current_item].visual_effect :fade if @line_item.quantity < 1
page[:current_item].visual_effect :highlight,
:startcolor => "#cc0000",
:endcolor => "#114411"
Nicolas says:
I’d start by adding a remove_from_cart method to store_controller.rb:
def remove_from_cart
begin
product = Product.find(params[:id])
rescue ActiveRecord::RecordNotFound
logger.error("Attempt to access invalid product #{params[:id]}")
redirect_to_index("Invalid product" )
else
@cart = find_cart
@current_item = @cart.remove_product(product)
redirect_to_index unless request.xhr?
end
end
I’d then add the corresponding remove_product(product) method to cart.rb
def remove_product(product)
current_item = @items.find {|item| item.product == product}
current_item.decrement_quantity
if current_item.quantity == 0
@items.delete(current_item)
end
current_item
end
And add the decrement_quantity method to cart_item.rb (this and the increment_quantity could be merged into one method)
def decrement_quantity
@quantity -= 1 if @quantity > 0
end
Then it’s time to edit the templates. Starting with _cart_item.rhtml: (lines preceded with ”>” have been added to the file’s previous state in the tutorial)
<% if cart_item == @current_item %>
<tr id="current_item">
<% else %>
<tr>
<% end %>
<td><%= cart_item.quantity %>×</td>
<td><%= h(cart_item.title) %></td>
<td class="item-price" ><%= number_to_currency(cart_item.price) %></td>
> <td>
> <%= link_to_remote "remove", :url => { :action => :remove_from_cart , :id => cart_item.product} %>
> </td>
</tr>
The link_to_remote method is an alternative to using the form_remote_tag method, and outputs a link instead of a button, which is more suitable to a “remove” action.
Bashar Asks:
Isn’t link_to_remote dropped from Rails 3? alternatively we should use :remote => true ? as for me, it gave Undefined Method ‘link_to_remote’
Edward says: Playing with the _cart_item.html.erb I end up with:
<% if cart_item == @current_item %>
<tr id="current_item">
<% else %>
<tr>
<% end %>
<td><%= cart_item.quantity %>×</td>
<td><%=h cart_item.title %></td>
<td class="item-price"><%= number_to_currency(cart_item.price)%></td>
<td>
<% if cart_item.quantity > 1 %>
<% form_remote_tag :url => {:action => 'subtract_from_cart', :id => cart_item.product } do %>
<%= submit_tag " Subtract" %>
<% end %>
<% else %>
<% form_remote_tag :url => {:action => 'subtract_from_cart', :id => cart_item.product } do %>
<%= submit_tag " Remove" %>
<% end %>
<% end %>
</td>
</tr>
Matt says:
This didn’t quite work for me. The above code didn’t cause the cart to be updated when I removed an item. To make it work I first used a slightly different remove_from_cart method in store_controller.rb
def remove_from_cart
begin
product = Product.find(params[:id])
rescue ActiveRecord::RecordNotFound
logger.error("Attempt to access invalid product #{params[:id]}")
redirect_to_index("Invalid product" )
else
@cart = find_cart
@current_item = @cart.remove_product(product)
respond_to do |format|
format.js if request.xhr?
format.html {redirect_to_index}
end
end
end
I then had to go and create a remove_from_cart.js.rjs template in store/views:
page.replace_html("cart", :partial => "cart", :object => @cart)
page[:cart].visual_effect :blind_up if @cart.total_items == 0
I found this seemed to work quite well except that items were being decremented twice. Changing the link_to_remote call in the _cart_item.html.erb template did the trick to clear that up:
<td><%= link_to "remove", :action => :remove_from_cart, :id => cart_item.product %></td>
Axel Christ asks:
Why is a link more suitable for a »remove« action? Any specific reason?
k9d answers:
It’s an aesthetic/preference thing, the page looks goofy with so many large buttons screaming REMOVE!!! Can you see the sales vanishing into thin air? No? Then you need more AJAX!
Bradford Chang says:
This seems to work. There is a problem, though, if javascript is disabled. It works fine if you use a form_remote_tag rather than the link_to_remote. Is there a way to get this working sans javascript using a link_to_remote call (without using a form button)?
meowsqueak answers:
To make the link_to_remote work when javascript is disabled, you might need to provide a fallback URL (with :href) :
<%= link_to_remote "remove", {:url => { :action => :remove_from_cart, :id => cart_item.product}}, { :href => url_for(:action => :remove_from_cart, :id => cart_item.product) } %>
k9d answers:
Another javascript-less solution using a plain old link_to, similar to exercise PT-B-2. The javascript-less remove link in _cart_item.hrtml looks like this (if you use it, you’ll have to ajaxify differently than how Nicolas proceeds):
<%= link_to "remove", :action => :remove_from_cart , :id => cart_item.product %>
Testing at this stage should work fine, nothing will visibly change when you click the “remove” link for any cart_item. However, manually refreshing the page shows a decremented product count.
Now time to add AJAX. Create a file remove_from_cart.rjs:
page.replace_html("cart", :partial => "cart", :object => @cart)
if @cart.total_items == 0
page[:cart].visual_effect :blind_up
elsif @current_item.quantity > 0
page[:current_item].visual_effect :highlight,
:startcolor => "#FF8888",
:endcolor => "#441111"
end
As you can see, we don’t want to call the highlight effect for either of the following two conditions:
The item removal effectively empties the cart completely (no more items to highlight) The item removal reduces the item count to 0, deleting it from the cart (cart_item no longer available to highlight) I’ve also changed the highlight color to red, which is more consistent with a deletion. When the cart is empty, the blind_up effect hides the cart div. One improvement would be to stop the total_price from updating when the cart is empty, since it is not useful and looks a bit like a glitch.
RL asks: How is AJAX call possible with just plain link_to? Can you elaborate?
h4. ilari says: You can stop the total price from updating when the cart is empty with the following:
page.replace_html("cart", :partial => "cart", :object => @cart) unless @cart.total_items == 0
James_H says:
Did any one else have trouble getting blind_up to work when removing items from the cart?
Derek I answers:
‘_cart.rhtml’ hides the cart div when empty so there is nothing to shrink when removing that last item. Remove the ‘<% unless cart.items.empty? %>’ condition from _cart.rhtml to catch a glimpse. )
Derek I asks:
Also, I get the occasional HTTP 500 response when adding to my cart. The page will send a request as follows: commit=Add%20to%20Cart&_=. It’s almost as if the Ajax request didn’t get a chance to form before the regular HTTP request was formed. Weird! Thoughts anyone?
SydneyStephen? says on 11 Juanuary 2007:
I also get the HTTP 500 response if I press the add-to-cart button too quickly after emptying the cart or adding another item. I get a very inelegant error message. However if I repeat the operation more slowly the error does not occur:
Application error
Change this error message for exceptions thrown outside of an action (like in Dispatcher setups or broken Ruby code) in public/500.html
Marcello asks:
Anyone found a way to be more DRY with te add_product and remove_product methods?
DonH answers:
I tried to a little magic with blocks and yielding.
I started by making a private method manage_cart in the store_controller.rb:
def manage_cart
begin
product = Product.find(params[:id])
p product.title
rescue ActiveRecord::RecordNotFound
logger.error("Attempted to access invalid product #{params[:id]}")
redirect_to_index("Could not find Product")
else
@cart = find_cart
@current_item = yield
redirect_to_index unless request.xhr?
end
end
Bill asks:
What is this line (p product.title) for?
Then I refactored both the add_to_cart and remove_from_cart to look like this:
def add_to_cart
manage_cart {@cart.add_product(product)}
end
def remove_from_cart
manage_cart {@cart.remove_product(product)}
end
The problem that I seem to be running into is all goes well till we hit the @current_item = yield, then everything just stops. Any suggestions? Am I using the blocks correctly, I was hoping to do it this way as opposed to added a marker token in the request parameters and doing a bunch of control logic with decision code.
Any input would be appreciated.
DonH:
Okay I got it figured, the calls in the add_to_cart and remove_from_cart need to be modified to:
def add_to_cart
manage_cart {|product| @cart.add_product(product)}
end
def remove_from_cart
manage_cart {|product| @cart.remove_product(product)}
end
And the call to yield should be: @current_item = yield product
Anyone come up with a better way?
El_gringo says on 27 March 2007:
I think we shouldn’t have to make an SQL request for removing a product from the cart : we have all needed informations in the CartItems? objects, so an SQL access is not needed. Here are differences between my solution and the one of Nicolas :
In store_controller.rb :
def remove_from_cart
@cart = find_cart
product_id = params[:id].to_i
@current_item = @cart.remove_product(product_id)
redirect_to_index
end
And in cart.rb :
def remove_product(product_id)
current_item = @items.find {|item| item.product.id == product_id}
current_item.decrement_quantity
if current_item.empty?
@items.delete(current_item)
end
current_item
end
jpl says on 08 April 2007:
El_gringo, your solution does not handle “Invalid product”: what if someone makes a request to /store/remove_from_cart/-1 ? OK, nobody will ever do it. But you shouldn’t care about this optimization: actually, this optimization could exist in the implementation of Product.find ( imagine a cache mechanism that prevents some DB access). Anyway the session is stored in the DB so there will be at least one request to the DB.
Anthony says:
I like the last solution, most elegant in my opinion. I still don’t like the idea of using a GET request to perform a database modification. A button can be used and styled appropriately.
I had one modification to El_gringo’s delete function:
if current_item.empty?
changed to:
if current_item.quantity < 1
Otherwise, I was getting negative quantities in the cart.
Steven said:
One of the previous “Playtime” tasks was to change the book icons to clickable elements that could also be used to add the book to the cart. Is there a way to continue this feature with Ajax?
Onno says:
To get that working, combine the original link for the image with link_to_remote mentioned earlier here. You’ll get the next line for the image-link in store/index.rhtml:
<%= link_to_remote image_tag(product.image_url), {:url => { :action => :add_to_cart, :id => product }}, {:href => url_for(:action => :add_to_cart, :id => product) } %>
Patrick says:
I was having a problem with the format of the cart table, what with things sometimes taking up 2 lines sometimes 1. It looked messy. also I thought it’d be nice if we could add to the cart from the cart itself since we can remove items from it there. So I used Nicolas and meowsqeak and tweaked it a bit for my purposes. The only thing different is my _cart_item.rhtml. I added two td columns. In one, I separated the product.quantity from the times symbol, and in the other I added an add to cart link. I also made “add” and “remove” into ”+” and ”-” respectively to save space and not overly encourage our customers to remove products. Here is my _cart_item.rhtml:
<% if cart_item == @current_item %>
<tr id="current_item">
<% else %>
<tr>
<% end %>
<td><%= cart_item.quantity %> </td>
<td> × </td>
<td><%= h(cart_item.title) %></td>
<td class="item-price"><%= number_to_currency(cart_item.price) %></td>
<td>
<%= link_to_remote "+", {:url => { :action => :add_to_cart, :id => cart_item.product}}, { :href => url_for(:action => :add_to_cart, :id => cart_item.product) } %>
</td>
<td>
<%= link_to_remote "-", {:url => { :action => :remove_from_cart, :id => cart_item.product}}, { :href => url_for(:action => :remove_from_cart, :id => cart_item.product) } %>
</td>
</tr>
This works with or without java. My one question is, for the fall back url, does it call a POST request even though its an href or is it sending a GET request?
dkusleika says:
If you attempt to remove an item (a legitimate product) that does not exist in the cart, you will get an error. At least I did. My store_controller remove_from_cart looks like
def remove_from_cart
begin
product = Product.find(params[:id])
rescue ActiveRecord::RecordNotFound
logger.error("Attempt to access invalid product #{params[:id]}")
redirect_to_index("Invalid product" )
else
@cart = find_cart
if @cart.has_product(product)
@current_item = @cart.remove_product(product)
redirect_to_index unless request.xhr?
else
redirect_to_index
end
end
end
I’m sure there’s a clean way to implement has_product, but
def has_product(product)
current_item = @items.find {|item| item.product == product}
if current_item
true
else
false
end
end
If the product id is bogus, the store_controller will catch it. If it’s a good product id, but just doesn’t happen to be in the cart, has_product will return false and redirect_to_index will be called – and basically nothing else done. Also, I implemented my remove_product slightly different
def remove_product(product)
current_item = @items.find {|item| item.product == product}
if current_item.quantity < 2
@items.delete(current_item)
else
current_item.decrement_quantity
current_item
end
end
I only call decrement_quantity if I need to. I doubt this makes much difference.
If someone can show me how to write has_product correctly, or if there’s a better way to check, I’d appreciate it.
Scott says: The following code worked for me (Rails 2.0.2). Thanks to all who contributed to this thread, which helped me get this working.
store_controller.rb
# ...
def remove_from_cart
begin
product = Product.find(params[:id])
rescue ActiveRecord::RecordNotFound
logger.error("Attempt to access invalid product #{params[:id]}")
redirect_to_index "Invalid product"
else
@cart = find_cart
@current_item = @cart.remove_product(product)
if request.xhr?
respond_to {|format| format.js}
else
redirect_to_index
end
end
end
cart.rb
# ...
def remove_product(product)
current_item = @items.find {|item| item.product == product}
current_item.decrement_quantity
if current_item.quantity == 0
@items.delete(current_item)
end
current_item
end
cart_item.rb
# ...
def decrement_quantity
@quantity -= 1 if @quantity > 0
end
_cart_item.html.erb
# ...
<%= link_to_remote "×",
{:url => {:action => :remove_from_cart, :id => cart_item.product}},
{:href => url_for(:action => :remove_from_cart, :id => cart_item.product)} %>
remove_from_cart.js.rjs
if @cart.total_items == 0
page[:cart].visual_effect :blind_up
else
page.replace_html("cart", :partial => "cart", :object => @cart)
if @current_item.quantity > 0
page[:current_item].visual_effect :highlight, :startcolor => "#FF8888", :endcolor => "#114411"
end
end
SSJordan says: I got this exercise working.
However, in reading k9d’s decision to change highlight color to red, it dawned on me that these highlight colors should probably be controlled by CCS.
How would you hook a style class in the blind_up and blind_down effects that can then be controlled through CSS?
amanfredi says: I noticed that combining this with the grow effect in the previous playtime exercise will likely result in your cart item tr becoming hidden when you decrement the quantity to 1. To fix this I needed to know which action caused the ajax update to the cart, so I set an instance variable ”@cart_action” to the name of the method called in store_controller.rb. This way I could check which action actually caused the cart_item quantity to become 1.
If there’s a better way to avoid this problem, I’d love to hear some suggestions.
h4. SilviuB says: I have used following code for remove_from_cart method in store_controller.rb. This is to cache all possible errors:
def remove_from_cart
begin
product = Product.find(params[:id])
@cart = find_cart
@current_item = @cart.remove_product(product)
rescue NoMethodError
logger.error("Attempt to access a product which is not in your cart #{params[:id]}")
redirect_to_index "Product not in your cart!"
rescue ActiveRecord::RecordNotFound
logger.error("Attempt to access invalid product #{params[:id]}")
redirect_to_index "Invalid product!"
else
respond_to do |format|
format.js if request.xhr?
format.html { redirect_to_index }
end
end
end
Isn’t better/safer to use:
<td>
<% form_remote_tag :url => {:action => :remove_from_cart, :id => cart_item.product} do -%>
<%= submit_tag "-" %>
<% end -%>
</td>
instead of link_to_remote?
JinYoung: OK. I’ve done like below.
In store_controller.rb
def remove_from_cart
begin
product = Product.find(params[:id])
rescue ActiveRecord::RecordNotFound
logger.error("Attempt to access invalid product #{params[:id]}" )
redirect_to_index("Invalid product")
else
@cart = find_cart
@current_item = @cart.remove_product(product)
respond_to do |format|
format.js if request.xhr?
format.html { redirect_to_index("One #{product.title} was removed") }
end
end
end
In _cart.html.erb, I’ve removed first and last line(<% unless cart.items.empty? >, < end %>) and add colspan attribute to “total-cell” td tag
<td colspan="2" class="total-cell"><%= number_to_currency(cart.total_price) %></td>
In cart.rb
def remove_product(product)
current_item = @items.find {|item| item.product == product}
if current_item
current_item.decrement_quantity
@items.delete(current_item) if current_item.quantity == 0
end
current_item
end
Add new file ‘remove_from_cart.js.rjs’
page.replace_html("cart" , :partial => "cart" , :object => @cart)
page[:current_item].visual_effect :highlight, :startcolor => "#88ff88" , :endcolor => "#114411" if @current_item.quantity >= 1
Finally, I’ve added delete button column to the cart table. In _cart_item.html.erb
<td>
<% form_remote_tag :url => { :action => ((@cart.items.length == 1 and cart_item.quantity == 1) ? :empty_cart : :remove_from_cart), :id => cart_item.product } do %>
<%= submit_tag "D" %>
<% end %>
</td>
I think the “(@cart.items.length 1 and cart_item.quantity 1)” part could raise to a controversy. At first, I had done it with another way.
In remove_from_cart.js.rjs
page.replace_html("cart" , :partial => "cart" , :object => @cart)
page[:current_item].visual_effect :highlight, :startcolor => "#88ff88" , :endcolor => "#114411" if @current_item.quantity >= 1
page[:cart].visual_effect :blind_up if @cart.items.length == 0
In _cart_item.html.erb
<td>
<% form_remote_tag :url => { :action => :remove_from_cart, :id => cart_item.product } do %>
<%= submit_tag "D" %>
<% end %>
</td>
However, this way is not good for display effect. So I changed some code to call empty_cart when the cart item is last one of our cart.
h4. Daniel says: I added id to the cart_item model: cart_item.rb
def id
@product.id
end
I added has_item(id) to the cart model
cart.rb
def has_item(id)
this_item = @items.find {|item| item.id == id.to_i}
if this_item
true
else
false
end
end
This searches for a cart item with given id similar to dkusleika’s version but as you will see in remove_from_cart I won’t do any database querry.
Maybe there is a product with that id in my database, but I don’t care since the only thing I want to know is, if there is a product with that id in my cart.
def remove_from_cart
@cart = find_cart
if @cart.has_item(params[:id])
@current_item = @cart.remove_item(params[:id])
respond_to do |format|
format.js if request.xhr?
format.html {redirect_to_index}
end
else
logger.error("EE :: Item not in cart #{params[:id]}")
redirect_to_index
end
end
Manuel K. says:
I used the generated LineItems#destroy method.
- First I changed the LineItems#destory method to fit my needs:
def destroy @cart = find_or_create_cart product = Product.find(params[:product_id]) @cart.delete_product(product.id) @current_item = @cart.line_items.where(:product_id => product.id).first respond_to do |format| format.html { redirect_to(store_url) } format.js format.xml { head :ok } end end - Afterwards I implemented the delete_product method in the Cart model:
def delete_product(product_id) current_item = line_items.where(:product_id => product_id).first unless current_item.nil? if current_item.quantity > 1 current_item.quantity -= 1 line_items << current_item else current_item.destroy end end - Then I added a button_to call to the index.html.erb view for the store controller:
<%= button_to ‘Remove from Cart’, line_items_path(:product_id => product.id), :method =>:delete, :remote => true %>
- I figured out, that this doesn’t work, so I changed the routes et voilá:
I don’t why this works, but everytime I click “Remove from Cart” the server log prints out, that it does a POST request instead of a DELETE request. I’t would be very kind if some rails guru could help me :-). It work’s but I don’t know why…delete "line_items" => 'line_items#destroy' - Last but not least I did all the scrip.aculo.us stuff. Okay, I copied almost everything from “k9d” , but the line in bold letters was modified by me:
And to make this work I added format.js to the LineItems#destroy method, and added the :remote => true option to the button_to call. These changes are integrated in the code snipptes I posted above.if @cart.total_items == 0 page[:cart].visual_effect :blind_up else page.replace_html("cart", render(@cart))unless @current_item.nil?page[:current_item].visual_effect :highlight, :startcolor => "#FF8888", :endcolor => "#441111" end end
Charlie says:
Hi Manuel, i also use the RESTful _path thing, and it works, FYI. (but i pass the line_item’s id instead of product’s id for the path reference)
- here is what i have in the view
<td><%= link_to ('Delete one', line_item_path(line_item.id),:method => :delete,:confirm => 'Are you sure?') %></td>
i think the point here is to use line_item_path instead of line_items_path, if you want to use :method=>delete…please correct me if i am wrong.
- and here is what i have in line_item controller,
def destroy @cart = current_cart @line_item = @cart.deduct_product(params[:id]) respond_to do |format|.... - in cart.rb:
def deduct_product(line_item_id) current_item = LineItem.find(line_item_id) if current_item.quantity > 1 current_item.quantity -= 1 current_item.save else current_item.destroy end current_item end
in the beginning i forgot to add current_item.save, so the quantity never changed… is it equal to line_items << current_item?...
Robb asks …
Ruby 1.9.2-p136, Rails 3.0.3
Am I wildly off-base to try to perform the decrement within line_item only? It seems odd to me to call out into the Cart model and run those extra queries to find the line_item that is already available to me as @line_item in the line_item controller.
One notion I have (as an admitted RoR newb) is to do a button_to with line_item_path and :method => :delete. I’d like to add a check for @line_item.quantity > 1 in the line_item destroy action and if true to somehow call the line_item update action with a decremented quantity passed as a parameter. If false, destroy would continue with the call to @line_item.destroy. It’s the “somehow call the update action” that I cannot figure. With a redirect_to? Is there a way?
—Robb (Dec 29, 2010)

