Introduction

One of the many questions i've seen out there on the web is 'how do I build a shopping cart?' This article will attempt to answer that question. In this article, we will build up a simple shopping cart that can be easily expanded upon. Let's get started.

Rails Application Setup

The first thing we will need to do is create our models. For this example we will need 4 different models:

  • Product — Stores product information for products that are sellable.
  • Order Status — Determines the status of an order. For instance, 'In Progress' means it's just a shopping cart, 'Completed' means it's completed, etc.
  • Order — Summarizes the order information. Things like the subtotal are stored/calculated here.
  • OrderItem — Stores detailed order item information.

Run the commands below to generate these models now.

Edit: As some of my readers have pointed out, you need to wrap the decimal fields in quotes. This was a recent Rails change. The commands below should work from 4.1 up.

Terminal Commands:

rails g model Product name 'price:decimal{12,3}' active:boolean
rails g model OrderStatus name:string
rails g model Order 'subtotal:decimal{12,3}' 'tax:decimal{12,3}' 'shipping:decimal{12,3}' 'total:decimal{12,3}' order_status:references
rails g model OrderItem product:references order:references 'unit_price:decimal{12,3}' quantity:integer 'total_price:decimal{12,3}'
rake db:migrate

The next thing we need to do is create some seed data. Open up your db/seeds.rb file and modify it so that it looks like the code listed below.

db/seeds.rb:

Product.delete_all
Product.create! id: 1, name: "Banana", price: 0.49, active: true
Product.create! id: 2, name: "Apple", price: 0.29, active: true
Product.create! id: 3, name: "Carton of Strawberries", price: 1.99, active: true

OrderStatus.delete_all
OrderStatus.create! id: 1, name: "In Progress"
OrderStatus.create! id: 2, name: "Placed"
OrderStatus.create! id: 3, name: "Shipped"
OrderStatus.create! id: 4, name: "Cancelled"

Now run a rake db:seed to insert the seed data into your database.

Terminal Commands:

rake db:seed

Next we need to add some code to our model. Open up your order model and modify it so that it looks like the code listed below.

app/models/order.rb:

class Order < ActiveRecord::Base
  belongs_to :order_status
  has_many :order_items
  before_create :set_order_status
  before_save :update_subtotal

  def subtotal
    order_items.collect { |oi| oi.valid? ? (oi.quantity * oi.unit_price) : 0 }.sum
  end
private
  def set_order_status
    self.order_status_id = 1
  end

  def update_subtotal
    self[:subtotal] = subtotal
  end
end

In the model above, the first 2 lines set up our associations. The next 2 lines set up a couple callbacks. The set_order_status gets fired when the order is created and sets the order_status_id column to 1 (in progress). The update_subtotal function is called during save and sums up our order item's total cost and stores it in the subtotal field. The subtotal function actually overrides the field named subtotal, so calling order.subtotal calls the function. You can still access the field internally by calling self[:subtotal], so we use that to update the field. This allows us to dynamically update subtotal as needed. A side effect of this is user can't write any arbitrary value to the subtotal field.

Now let's add some code to our OrderItem model. Open up the OrderItem model and add in the code listed below.

app/models/order_item.rb:

class OrderItem < ActiveRecord::Base
  belongs_to :product
  belongs_to :order

  validates :quantity, presence: true, numericality: { only_integer: true, greater_than: 0 }
  validate :product_present
  validate :order_present

  before_save :finalize

  def unit_price
    if persisted?
      self[:unit_price]
    else
      product.price
    end
  end

  def total_price
    unit_price * quantity
  end

private
  def product_present
    if product.nil?
      errors.add(:product, "is not valid or is not active.")
    end
  end

  def order_present
    if order.nil?
      errors.add(:order, "is not a valid order.")
    end
  end

  def finalize
    self[:unit_price] = unit_price
    self[:total_price] = quantity * self[:unit_price]
  end
end

The first thing to note are the validations. The first validation for quantity ensures that the quantity is a number that is an integer and is greater than 0. The next validation is a custom validation that ensures that the product is present and valid. The final validation for order determines if the order is present and valid.

The next thing to note here is the unit_price function. The unit_price function will take the associated product's price if the order item is not currently persisted. If the order item is persisted then the contents of the unit_price field will be returned instead. Before the model is saved, the return value of unit_price is written to the unit_price field. This means that once the product is added to our cart, if the product changes price the user will still be able to buy the product at the previous price. This is necessary because if the price changes while the user is in checkout, there could be a mismatch between the product price on the cart page and the product price on the checkout page. You can always modify this code to fit your own needs.

The finalize function gets called before save and updates the unit_price and total_price fields with the proper values.

Phew! Now that we are done with that, let's ensure that our OrderStatus model is set up properly. Open up your OrderStatus model and verify that it looks like the code listed below.

app/models/order_status.rb:

class OrderStatus < ActiveRecord::Base
  has_many :orders
end

Great, now let's add some code into our Product model. Open up your Product model and modify it so that it looks like the code listed below.

app/models/product.rb:

class Product < ActiveRecord::Base
  has_many :order_items

  default_scope { where(active: true) }
end

All we did here is add a default scope that checks if the active flag is set to true. This ensures that deleted/inactive products aren't shown.

Great! Now let's move onto our controllers. For this example we will create 3 controllers. The Products controller will display a list of products for sale. The Carts controller will show the contents of our shopping cart. Finally the OrderItems controller will take care of the dirty work of manipulating our shopping cart. Run the commands below to generate these controllers now.

Terminal Commands:

rails g controller Products index
rails g controller Carts show
rails g controller OrderItems create update destroy

Now let's modify our routes file to properly set up the routes. Open up your routes file and add in the code listed below.

config/routes.rb:

Rails.application.routes.draw do
  resources :products, only: [:index]
  resource :cart, only: [:show]
  resources :order_items, only: [:create, :update, :destroy]
  root to: "products#index"
end

Now let's add in a bit of code to our ApplicationController. In our ApplicationController we will add in a method called current_order that will return the current order or create a new order if none exists. Open up your application controller and modify it so that it looks like the code listed below.

app/controllers/application_controller.rb:

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  helper_method :current_order

  def current_order
    if !session[:order_id].nil?
      Order.find(session[:order_id])
    else
      Order.new
    end
  end
end

Great, now let's open up our CartsController and add some code that will allow us to render our cart. Open up the CartsController and modify it so that it looks like the code listed below.

app/controllers/carts_controller.rb:

class CartsController < ApplicationController
  def show
    @order_items = current_order.order_items
  end
end

Now let's tackle the OrderItems controller. Open up your OrderItems controller and modify it so that it looks like the code listed below.

app/controllers/order_items_controller.rb:

class OrderItemsController < ApplicationController
  def create
    @order = current_order
    @order_item = @order.order_items.new(order_item_params)
    @order.save
    session[:order_id] = @order.id
  end

  def update
    @order = current_order
    @order_item = @order.order_items.find(params[:id])
    @order_item.update_attributes(order_item_params)
    @order_items = @order.order_items
  end

  def destroy
    @order = current_order
    @order_item = @order.order_items.find(params[:id])
    @order_item.destroy
    @order_items = @order.order_items
  end
private
  def order_item_params
    params.require(:order_item).permit(:quantity, :product_id)
  end
end

If you notice the @order.save call in the new method, it works like this: the first time the user adds an order_item to his cart, the new order is persisted to the database. From there on, the order's state is saved every time an order_item is added.

Now let's add some code to our ProductsController. Open up your Products controller and add in the code listed below.

app/controllers/products_controller.rb:

class ProductsController < ApplicationController
  def index
    @products = Product.all
    @order_item = current_order.order_items.new
  end
end

The only thing special about the products controller is it creates a new instance of our OrderItem model for use in our forms.

Alright! We are almost there. It's time for the views. Let's start with our application layout. Open up your application layout and modify it so that it looks like the code listed below.

app/views/layouts/application.html.erb:

<!DOCTYPE html>
<html>
<head>
  <title>ShoppingCartExample</title>
  <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
  <% if request.ssl? %>
    <%= stylesheet_link_tag 'https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css' %>
    <%= javascript_include_tag 'https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js' %>
  <% else %> 
    <%= stylesheet_link_tag 'http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css' %>
    <%= javascript_include_tag 'http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js' %>
  <% end %>
  <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true %>
  
  <%= csrf_meta_tags %>
</head>
<body>
  <div class="container">
    <div class="row">
      <div class="col-xs-6">
        <h1><%= link_to "My Store", root_path %></h1>
      </div>
      <div class="col-xs-6 text-right">
        <h1 class="cart-text"><%= render 'layouts/cart_text' %></h1>
      </div>
    </div>
    <hr>
    <%= yield %>
  </div>

</body>
</html>

In our layout we added bootstrap and created a basic layout for our application.

Now let's create a partial called cart_text in our layouts folder. The cart_text partial is used to render the current cart subtotal in the upper right corner of your web page. Create the cart_text partial now and add in the code listed below.

app/views/layouts/_cart_text.html.erb:

<%= link_to "#{current_order.order_items.size} Items in Cart ( #{number_to_currency current_order.subtotal} )", cart_path, class: "btn btn-link" %>

Great, now let's modify the show view for the Carts controller. The show view is a wrapper for a partial that renders a listing of all of the items that have been added to our cart. We wrap the partial with a div so that we can modify it using AJAX. Modify your show view so that it looks like the code listed below.

app/views/carts/show.html.erb:

<div class="shopping-cart">
  <%= render "shopping_cart" %>
</div>

Now let's create the shopping_cart partial. The shopping_cart partial is responsible for rendering the cart itself. Create the shopping_cart partial for the Carts controller and add in the code listed below.

app/views/carts/_shopping_cart.html.erb:

<% if [email protected]_item.nil? && @order_item.errors.any? %>
  <div class="alert alert-danger">
    <ul>
    <% @order_item.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>
<% if @order_items.size == 0 %>
  <p class="text-center">
    There are no items in your shopping cart.  Please <%= link_to "go back", root_path %> and add some items to your cart.
  </p>
<% else %>
  <% @order_items.each do |order_item| %>
    <%= render 'carts/cart_row', product: order_item.product, order_item: order_item, show_total: true %>
  <% end %>
<% end %>

Great, now let's create the cart_row partial. The cart_row partial is responsible for rendering each row of the cart. Create the cart_row partial for Carts and add in the code listed below.

app/views/carts/_cart_row.html.erb:

<div class="well">

  <div class="row">
    <div class="col-xs-8">
      <h4><%= product.name %></h4>
    </div>
    <div class="col-xs-4">
      
      <%= form_for order_item, remote: true do |f| %>
        <h4 class="text-right">Unit Price: <span style="color: green"><%= number_to_currency order_item.unit_price %></span></h4>
        <div class="row">
          <div class="col-xs-4">
            <%= f.number_field :quantity, value: order_item.quantity.to_i, class: "form-control", min: 1 %>
            <%= f.hidden_field :product_id, value: product.id %>
          </div>
          <div class="col-xs-8 text-right">
            <div class="btn-group">
              <%= f.submit "Update Quantity", class: "btn btn-primary" %>
              <%= link_to "Delete", order_item, { data: { confirm: "Are you sure you wish to delete the product '#{order_item.product.name}' from your cart?" }, method: :delete, remote: true, class: "btn btn-danger" } %>
            </div>
          </div>
        </div>
        <h4 class="text-right">Total Price: <span style="color: green"><%= number_to_currency order_item.total_price %></span></h4>
      <% end %>
    </div>
    
  </div>
</div>

You'll notice we make extensive use of remote: true. This shopping cart is completely AJAX driven. Don't worry, the rest is easy!

Next we will need to create a view called create.js.erb for our OrderItems controller. This view will render some JavaScript as a response to an item being added to the cart. Create the create.js.erb view for OrderItems and add in the code listed below.

app/views/order_items/create.js.erb:

<% if @order.errors.any? || @order_item.errors.any? %>
alert("not valid.")
<% else %>
  $(".cart-text").html("<%= escape_javascript(render 'layouts/cart_text') %>")
<% end %>

Now let's create the destroy.js.erb view for carts. This view will return some js that is used to manipulate the cart when an item is removed. Create the destroy.js.erb view for Carts and add in the code listed below.

app/views/order_items/destroy.js.erb:

$(".cart-text").html("<%= escape_javascript(render 'layouts/cart_text') %>")
$(".shopping-cart").html("<%= escape_javascript(render 'carts/shopping_cart') %>")

Let's create one final view for OrderItems called update.js.erb. The update.js.erb view is rendered whenever a quantity is changed or the order item is updated. Create the update.js.erb view for OrderItems and add in the code listed below.

app/views/order_items/update.js.erb:

$(".cart-text").html("<%= escape_javascript(render 'layouts/cart_text') %>")
$(".shopping-cart").html("<%= escape_javascript(render 'carts/shopping_cart') %>")

Phew! Almost there! Now open up the index view for the Products controller and add in the code listed below.

app/views/products/index.html.erb:

<h3 class="text-center">Products for Sale</h3>

<div class="row">
  <div class="col-xs-6 col-xs-offset-3">
    <% @products.each do |product| %>
      <%= render "product_row", product: product, order_item: @order_item %>
    <% end %>
  </div>
</div>

The index view renders the list of products that are for sale.

Now let's add one final partial for the Products controller called _product_row, The product_row partial actually renders each product. Create the product_row partial for the Products controller and add in the code listed below.

app/views/products/_product_row.html.erb:

<div class="well">

  <div class="row">
    <div class="col-xs-8">
      <h4><%= product.name %></small></h4>
    </div>
    <div class="col-xs-4">
      
      <%= form_for order_item, remote: true do |f| %>
      <h4 class="text-right">Unit Price: <span style="color: green"><%= number_to_currency product.price %></span></h4>
        <div class="input-group">
          <%= f.number_field :quantity, value: 1, class: "form-control", min: 1 %>
          <div class="input-group-btn">
            <%= f.hidden_field :product_id, value: product.id %>
            <%= f.submit "Add to Cart", class: "btn btn-primary" %>
          </div>
        </div>
      <% end %>
    </div>
    
  </div>
</div>

Great! We are all finished! Now if you start a rails server and navigate to http://localhost:3000 you will see that you have a functional shopping cart! That's it! Thanks for reading!