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