0

Tính năng Giỏ Hàng, Shopping cart trong Rails

Ở bài viết này mình sẽ tóm tắt cách dùng Ruby on Rails và Ajax để tạo 1 app có chức năng "thêm vào giỏ hàng" như các shop online. ! 😊😊
Viết bài này cũng là để chính bản thân đọc lại khi có thời gian nên sẽ rất ngắn gọn và lược đi phần frontend. 😂

Mở đầu

App lần này của mình sẽ là app order sách.

Ruby version: 2.7.2
Rails version : 6.1.5
Database : default SQLite3

1. tạo user bằng gem devise

  • Bước này khá đơn giản nên mình sẽ không nói sâu nữa.
    Thêm gem 'devise' vào Gemfile. Sau đó các bạn cài đặt devise như trong hướng dẫn của github https://github.com/heartcombo/devise
  • Lưu ý rằng ở đây mình sẽ thay tên class name của mình là user chứ không phải MODEL như trong hướng dẫn.

2. Tạo Model và Controller

  • Tạo Model booktitle là tên sách và price là giá sách
rails g model book title:text price:integer

  • Controller books với 2 chức năng là create và destroy
rails g controller books create destroy

  • Model order dùng để lưu lại lịch sử order. quantity là số lượng mỗi lần order, total_price là số tiền mỗi lần order. Trong table order thì book_iduser_idsẽ là khóa ngoại.
rails g model order quantity:integer total_price:integer book:references user:references

  • Controller orders có 2 chức năng là create và destroy
rails g controller orders create destroy

  • Tạo trang homepage cho app qua file index của controller homes
rails g controller homes index

* Khởi tạo database bằng lệnh migrate
rails db:migrate

3. Tạo tính năng Giỏ Hàng

  • Tạo database book bằng seed. App của mình sẽ có 3 sản phầm sách như sau:
    seed.rb
Book.create(title: "Java", price: 50)
Book.create(title: "RoRs", price: 80)
Book.create(title: "C", price: 70)
rails db:seed

  • Config file routes.rb
Rails.application.routes.draw do
  get 'books/create'
  get 'books/destroy'

  resources :orders, only: [:create, :destroy]
  resources :homes, only: [:index]
  root "homes#index"
  devise_for :users
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end

resources :orders chỉ áp dụng cho method createdestroy.
resources :homes chỉ áp dụng cho method index.


  • book.rb
class Book < ApplicationRecord
    has_many :orders
end
  • order.rb
class Order < ApplicationRecord
    belongs_to :book
    belongs_to :user
end

Một book sẽ có nhiều orders. Quan hệ giữa bookorders, cũng như giữa userorders là one-to-many.

  • Homepage của app sẽ được customize qua file homes/index.html.erb
<h1>Shop</h1>

<h4>Logging as: <%= current_user.email %> -- 
<%= link_to "Sign Out", destroy_user_session_path, method: :delete %></h4>

<% @books.each do |book| %>
    <%= book.id %> - <%= book.title %> - <%= book.price %>¥
    <%= form_for @order, remote: true do |f| %>
    	<%= f.hidden_field :book_id, :value => book.id %>
    	<%= f.hidden_field :user_id, :value => current_user.id %>
    	<%= f.number_field :quantity, :value => 1, :min => 1 %>
    	<%= f.submit "Add to cart" %>
	<% end %>
<% end %>

<hr>
<div class="total-amount">
    <%= render "orders/total" %>
</div>
<div class="reception">
    <% @orders.each do |order| %>
            <%= render "orders/order", order: order %>
    <% end %>
</div>



Do ở phần sau mình có dùng thêm tính năng của Ajax nên trong form_for phải có thêm remote: true.


  • Tạo mới file orders/_total.html.erb, tổng tiền cần thanh toán sẽ được gọi qua file partial này. *
<h3>Ordered List -- Total amount: <%= Order.where(user_id: current_user.id).sum(:total_price) %>¥</h3>

Tổng tiền sẽ được tính bằng tổng của tất cả column total_price trong bảng Order.
Ở trang chủ sẽ chỉ hiển thị những order của người dùng hiện tại chứ không phải tất cả orders.

  • Tạo mới file orders/_order.html.erb, danh sách order sẽ được hiển thị qua file partial này.
<%= order.created_at.strftime("%Y/%m/%d") %> - <%= order.book.title %> - 
<%= order.book.price %>¥ x <%= order.quantity %> = 
<%= order.total_price %>¥<br>

  • Config controller homesorders. Sửa file app/controller/homes_controller.rb như sau
class HomesController < ApplicationController
    before_action :authenticate_user!
    
  def index
      @books = Book.all
      @orders = Order.where(user_id: current_user.id).order("created_at DESC")
      @order = Order.new
  end
end

Ta muốn người dùng phải đăng nhập trước khi truy cập vào trang web. Do đó thêm before_action :authenticate_user! vào đầu controller.

  • app/controller/orders_controller.rb
class OrdersController < ApplicationController
  def create
    @order = Order.new(order_params)
    @order.total_price = @order.book.price * @order.quantity
    respond_to do |format|
        if @order.save
            format.js {} 
        end
    end
  end

  def destroy
  end

  private
  def order_params
    params.require(:order).permit(:book_id, :quantity, :total_price, :user_id)      
  end
end

4. Áp dụng Ajax

Đến đây tính năng Thêm vào giỏ hàng đã cơ bản được hoàn thành. Nhưng mình muốn áp dụng ajax vào app để các thao tác người dùng được mượt mà hơn. Các bạn có thể xem thêm cách xây dựng 1 app Ajax đơn giản qua bài mình đã viết đây

https://viblo.asia/p/app-ajax-don-gian-trong-rails-bWrZnAmrKxw


  • Import CDN của ajax vào file application.html.erb. Phiên bản mình dùng là 3.6.0.
<!DOCTYPE html>
<html>
  <head>
    <title>Testapp</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

  • Trong folder app/views/orders, đổi tên file create.html.erb thành create.js.erb . Sau mỗi lần bấm button Add to cart thì file create.js.erb sẽ được gọi.
$(".reception").prepend("<%= j render @order %>");
$(".total-amount").html("<%= j render "orders/total" %>");

Mỗi khi bấm button Add to cart thì thông tin book được đặt sẽ được hiển thị lên màn hình và tổng tiền sẽ được làm mới.

Câu lệnh <%= j render @order %> là cách viết tắt của <%= escape_javascript(render @order) %>.
Nó giúp lệnh render của chúng ta tránh bị lỗi ký tự khi render file partial. Các bạn có thể đọc thêm về escape_javascript ở đây
https://stackoverflow.com/questions/1620113/why-escape-javascript-before-rendering-a-partial Hoặc https://apidock.com/rails/ActionView/Helpers/JavaScriptHelper/escape_javascript


Đến đây app đặt sách đơn giản với tính năng thêm vào giỏ hàng đã được hoàn thành. Truy cập vào localhost và xem thành quả nào ^^.
Sau khi bấm Add to cart thì thông tin `book` được order sẽ được thêm vào Ordered List bên dưới.

ezgif.com-gif-maker (1).gif

Tổng kết

App bên trên do chỉ có tính năng thêm vào giỏ hàng nên vẫn còn khá sơ sài. Nhưng bằng cách làm tương tự, ta hoàn toàn có thể làm thêm tính năng xóa sản phẩm và chỉnh sửa số lượng sản phẩm trong ordered list.
Mọi người cùng đọc và góp ý nếu bài viết có thiếu sót nhé. 😊😊


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.