Skip to content
Ruby HTML CSS JavaScript CoffeeScript Dockerfile
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.devcontainer
_img
_old
app
bin
config
db
lib
log
public
storage
test
tmp
vendor
.browserslistrc
.gitignore
.ruby-version
Dockerfile
Gemfile
Gemfile.lock
README.md
README.pdf
Rakefile
babel.config.js
config.ru
docker-compose.yml
package.json
postcss.config.js
yarn.lock

README.md

Online Store - Ruby on Rails -Step by Step

Table of Contents

Dev Environment

  1. In VSCode -> remote dev plugin -> open in container ... -> choose docker-compsoe.yml.

  2. Install project dependencies: bundle install.

  3. Run server with rails server -b 0.0.0.0, you'll be able to access rails server on the host.

Create new application

ruby new myStore

Product Resource

Create a resource

  1. Generate scaffold for Product resource (default datatype is string)

    rails g scaffold Product name description:text image price:decimal
  2. Apply model migrations to create database.

    rails db:migrate

    Now you can access Products resource through localhost

    1-product.png

Additional: Change size of description in new Product

  1. Click New Product and the corresponding view is myStore/app/views/products/new.html.erb. Source the partial form at myStore/app/views/products/_form.html.erb.

  2. Find <%= form.text_area :description %> and add a row and column settings, and you can see the change by refreshing the page.

    <%= form.text_area :description, rows:10, cols:50 %>

    2-box-size.png

Data Constraints and Pre-Population

  1. Add validations to Product model at myStore/app/models/product.rb.

    product.rb contents
    class Product < ApplicationRecord
        # fields required
        validates :name, :description, :image, presence: true
        # numericality
        validates :price, 
            numericality: {greater_than_or_equal_to: 0.01}
        # uniqueness
        validates :name, uniqueness: true
        # RegEx
        validates :image, 
            allow_blank: true, 
            format: {
                with: %r{\.(gif|jpg|png)\Z}i, 
                message: 'must be GIF, JPG, PNG images'
            }
    end
  2. Add prepared data to myStore/db/seeds.rb

  3. Evaluate the population

    rails db:seed
  4. Refresh index page, see the populated list.

    3-populate

Product View style

  1. Modify stylesheet file myStore/app/views/products/_form.html.erb, and CSS usages are not covered here.

  2. Apply the stylesheet in index page file app/assets/stylesheets/products.scss. Refresh the index page and you'll see the style change.

    4-style.png

Shopper Controller

Create Controller

  1. Shopper's view uses the same Product resources, and we only need to create a shopper's controller.

    rails g controller Shopper index
  2. Modify routes.rb by changing get/ 'shopper/index/' as below, which makes url a little easier in latter usage.

    get 'shopper/', to: "shopper#index", :as => "shopper"

Shopper View style

  1. Modify the stylesheet at /workspaces/rails-store/myStore/app/assets/stylesheets/shopper.scss

  2. Change view of Shopper#index at myStore/app/views/shopper/index.html.erb, and apply the stylesheet above. Refresh http://localhost/shopper/index page and you'll see the shopper#index view.

    5-shopper.png

Add Page Layout

  1. Rename stylesheet file myStore/app/assets/stylesheets/application.css, to application.scss, and modify it as you need.

  2. Add layout tag to myStore/app/views/layouts/application.html.erb, and apply the stylesheet above. You'll see both the Admin view and shopper view get a banner and a side bar.

    6-layout-admin

    7-layout-shopper.png

Shopping Cart

Create resources

  1. Cart resource, similar to previous steps, and Cart resource is at http://localhost/carts

    rails g scaffold Cart
    
    rails db:migrate

    8-cart.png

  2. Lineitem resource recording relationship of cart and products, similar to previous steps.

    rails g scaffold Lineitem product:references cart:belongs_to quantity:integer
    • Assign default value to quantity by modify myStore/db/migrate/<timestamp>_create_lineitems.rb

      • Find the line t.integer :quantity
      • Change it to t.integer :quantity, default: 1
    • Apply model migrations to create Lineitem model.

      rails db:migrate

    Now you should able to see Lineitem resource at http://localhost/lineitems

    9-lineitem.png

Product, Cart, and Lineitem relations

  1. Lineitem serves as medium of many-to-many relationships between Product and Cart => One product can be in multiple carts and one cart can have multiple products.

  2. Product model in product.rb

    • One-many relation
      # one to many relationship
      has_many :lineitems
    • Key contraints
      # fields required
      validates :name, :description, :image, presence: true
      # numericality
      validates :price, 
          numericality: {greater_than_or_equal_to: 0.01}
      # uniqueness
      validates :name, uniqueness: true
      # RegEx
      validates :image, 
          allow_blank: true, 
          format: {
              with: %r{\.(gif|jpg|png)\Z}i, 
              message: 'must be GIF, JPG, PNG images'
          }
    • Callback function
      # callback function, called before 'destroy' method
      before_destroy :make_sure_no_line_items
      
      def make_sure_no_line_items
          if self.lineitems.empty?    # lineitems belong to product, and self will be implied if not explicitly written
              return true
          else
              return false
          end
      end
  3. Cart model in cart.rb

    • One-many and cascading relation
      # one to many relationship, cascading deletion
      has_many :lineitems, dependent: :destroy

Cart Initiation

  1. Helper method: set_cart in myStore/app/controllers/concerns/current_cart.rb

    set_cart
    # if session[:cart_id] exist, return the corresponding cart obj, 
    # else, create a new one with the corresponding session[:cart_id]
    def set_cart
        @cart = Cart.find(session[:cart_id])     
        rescue      # error handling
        @cart = Cart.create()
        session[:cart_id] = @cart.id
        return @cart
    end
  2. Initiaion: call set_cart as a before_action in myStore/app/controllers/application_controller.rb

    class ApplicationController < ActionController::Base
        # call back: set_cart
        include CurrentCart
        before_action :set_cart
        ...

Cart Button

  1. Add button CSS in shopper style sheet, and then add the following to shopper view. Refresh http://localhost/shopper/index page and you'll see the Add to Cart button.

    Add to Cart Button
    <%= button_to "Add to Cart", lineitems_path(product_id: product), class: "add_to_cart" %>

    10-add-cart.png

  2. Button action in lineitems_controller

    • POST '/lineitems' => lineitems#create

    • Redirect to shopper view afterwards.

    • lineitems_controller.rb
      def create
          # @lineitem = Lineitem.new(lineitem_params)
          product = Product.find(params[:product_id])
          @lineitem = @cart.lineitems.build(product: product)
      
          respond_to do |format|
          if @lineitem.save
              format.html { redirect_to shopper_url, notice: '%s is added to cart.' % [product.name] }
          ...
  3. Modify Cart#index view to display the contents of Cart, and then add items in http://localhost/shopper view, and you'll see items in the cart.

    Cart#index
    <p id="notice"><%= notice %></p>
    
    <h1>Your Shopping Cart</h1>
    
    <ol>
        <%# @cart obj contains multile Lineitem hash %>
        <% @cart.lineitems.each do |lineitem|%>
            <li><%= Product.find(lineitem[:product_id]).name %></li>
        <% end %>
    </ol>

    11-cart-function.png

Aggregate Cart items

  1. Create instance methods in Cart model

    • Add an item or increase the quality if exists.

    • Calculate total price

    • cart.rb
      def add_item(product_id)
          current_item = lineitems.find_by(product_id: product_id)
      
          # aggregate duplicate product
          if current_item == nil
              current_item = lineitems.build(product_id: product_id)
          else
              current_item.quantity += 1
          end
      
          return current_item
      end
      
      def total_price
          lineitems.inject(0) do |sum, lineitem|
              sum + lineitem.product.price * lineitem.quantity
          end
      end
  2. Change the lineitems_controller to use the instance method when adding an item to cart instance.

    lineitems_controller.rb
    def create
        # @lineitem = Lineitem.new(lineitem_params)
        product = Product.find(params[:product_id])
        @lineitem = @cart.add_item(product.id)
        ...
  3. Modify Cart view

    • Displya quality

    • Empty Cart button

    • lineitems_controller.rb
      <table>
          <thead>
              <th>Item name</th>
              <th>Quality</th>
              <th>Price</th>
          </thead>
      
          <% @cart.lineitems.each do |lineitem| %>
              <tr>
                  <td><%= lineitem.product.name %></td>
                  <td><%= lineitem.quantity %></td>
                  <td><%= lineitem.product.price * lineitem.quantity %></td>
              </tr>
          <% end %>
      
          <tr>
              <td colspan="2">Total</td>
              <td><%= @cart.total_price %></td>
          </tr>
      </table>
      <br>
      <div>
          <%= button_to 'Empty Cart', @cart, method: :delete, data: {confirm: 'Are you sure?'} %>
      </div>
  4. Modify carts_controller for Empty Cart button. Then you can 'll see change in localhost/carts

    carts_controller.rb
    # DELETE /carts/1
    # DELETE /carts/1.json
    def destroy
        @cart.destroy if @cart.id == session[:cart_id]
        session[:cart_id] = nil
        ...

    12-cart-aggregate.png

Display in side bar

  1. Create an _cart.html.erb partial with the same contents of cart view

  2. Render in layout

    • Set appearance in application.scss

    • Render the partial in layout, and you'll see the cart in the side bar.

    • layout
      ...
      <%# lower left %>
      <div id="side" >
          <div id="cart" >
              <%= render @cart %>
          </div>
      </div>
      ...

    13-cart-side.png

Partial Refresh

  1. To send ajax request from Add to Cart button, modify shopper view as:

    <%= button_to "Add to Cart", lineitems_path(product_id: product), class: "add_to_cart", remote:true %>
  2. Add the request in lineitems_controller

    lineitems_controller.rb
    # POST /lineitems.json
    def create
        ...
            format.html { redirect_to shopper_url, notice: '%s is added to cart.' % [product.name] }
            format.js
        ...
  3. Create the view, ESCAPE JS. Now if you click "Add to Cart" button, only the Cart part gets refreshed

    create.js.erb
    document.getElementById("cart").innerHTML = "<%= escape_javascript render @cart %>"
You can’t perform that action at this time.