Daily Archives: 24/11/2016

Rails – Tùy chỉnh giỏ hàng

Chúng ta sẽ thêm nút xóa giỏ hàng, hiển thị tổng tiền cho giỏ hàng.

Thêm nút xóa giỏ hàng

Đầu tiên chúng ta sửa lại file View show.html.erb trong thư mục app/views/carts như sau:

<%= notice %>
<h2>Your cart</h2>
<ul>
    <% @cart.line_items.each do |a| do %>
        <li><%= a.quantity %> x <%= a.product.title %></li>
    <% end %>
</ul>

<%= button_to 'Empty cart', 
              @cart, 
              :method => :delete,
              data: {:confirm => 'Are you sure?'} %>

Chúng ta gọi phương thức button_to để tạo một nút bấm với nhãn là ‘Empty cart’, URL chúng ta lấy từ biến @cart, nếu muốn bạn vẫn có thể dùng các phương thức như cart_path(), phương thức gửi lên là DELETE, tham số :data với thuộc tính :confirm sẽ tạo ra một hộp thoại như hàm alert() trong Javascript nếu bạn còn nhớ.

Ngoài ra chúng ta cũng đã xóa 2 nút EditBack do Rails tự tạo nếu bạn còn nhớ:

<%= link_to 'Edit', edit_cart_path(@cart) %> |
<%= link_to 'Back', carts_path %>

Nút ‘Empty cart’ sẽ gọi đến phương thức destroy trong lớp controller CartsController, trong trang /cart Rails cũng đã tạo cho chúng ta một nút như vậy với nhãn là ‘Destroy’ (nếu muốn bạn có thể vào xem thử), và mặc định phương thức destroy trong lớp Controller sẽ xóa giỏ hàng với id được truyền từ tham số, tuy nhiên ở đây chúng ta muốn phương thức này chỉ xóa giỏ hàng nào đang được lưu trong session của trình duyệt thôi, do đó chúng ta sửa lại phương thức destroy này như sau:

class CartsController < ApplicationController 
    .
    .
    .
    # DELETE /carts/1
    # DELETE /carts/1.json
    def destroy 
        @cart = current_cart
        @cart.destroy
        session[:cart_id] = nil
 
        respond_to do |format|
            format.html { redirect_to carts_url, notice: 'Cart was successfully destroyed.' }
            format.json { head :no_content }
        end
    end
    .
    .
    .
end

Chúng ta lấy model Cart hiện tại bằng phương thức current_cart đã được định nghĩa trong lớp ApplicationController, sau đó gọi hàm destroy để xóa, cuối cùng gán giá trị trong sessionnil.

Hiển thị tổng số tiền trong giỏ hàng

Chúng ta sửa lại file View show.html.erb một lần nữa như sau:

<%= notice %>
<h2>Your cart</h2>
<table>
    <% @cart.line_items.each do |item| %>
        <tr>
            <td><%= item.quantity %> x </td>
            <td><%= item.product.title %></td>
            <td class="item_price"><%= number_to_currency(item.total_price) %></td>
        </tr>
    <% end %>
    
    <tr class="total_line">
        <td colspan="2">Total</td>
        <td class="total_cell"><%= number_to_currency(@cart.total_price) %></td>
    </tr>
</table>

<%= button_to 'Empty cart', 
              @cart, 
              :method => :delete,
              data: {:confirm => 'Are you sure?'} %>

Ở đây thay vì dùng các thẻ <ul>, <li> để hiển thị danh sách thì chúng ta sẽ dùng bảng. Giá trị của tổng số tiền trên từng LineItem và tổng số tiền của Cart sẽ lấy từ phương thức total_price của lớp model tương ứng.

Các thẻ trên cũng dùng một số lớp CSS riêng, chúng ta định nghĩa các lớp CSS mới như sau:

.
.
.
#store .cart_title {
    font: 120% bold;
}

#store .item_price, #store .total_line {
    text-align: right;
}

#store .total_line .total_cell {
    font-weight: bold;
    border-top: 1px solid #595;
}

Và bây giờ chúng ta sẽ định nghĩa phương thức total_price cho từng model, đầu tiên chúng ta định nghĩa cho lớp LineItem như sau:

class LineItem < ActiveRecord::Base
    belongs_to :product
    belongs_to :cart
 
    def total_price
        product.price * quantity
    end
end

Rất đơn giản, số tiền bằng tổng số sản phẩm nhân với đơn giá sản phẩm.

Tiếp theo là lớp Cart:

class Cart < ActiveRecord::Base 
    has_many :line_items, :dependent => :destroy
 
    def add_product(product_id)
        current_item = line_items.find_by_product_id(product_id)
        if current_item 
            current_item.quantity += 1
        else
            current_item = line_items.build(:product_id => product_id)
        end
        current_item
    end
 
    def total_price 
        line_items.to_a.sum { |item| item.total_price }
    end
end

Ở đây số tiền sẽ bằng tổng số tiền của các đối tượng LineItem cộng lại. Phương thức to_a sẽ chuyển một đối tượng danh sách sang kiểu mảng, sau đó chúng ta gọi phương thức sum của mảng đó.

Bạn có thể xem tổng tiền và có thể xóa giỏ hàng được rồi:

capture

Ngoài ra nếu trước khi làm chức năng session mà bạn đã từng tạo nhiều giỏ hàng thì trong trang /carts (liệt kê danh sách giỏ hàng), trang này gọi đến phương thức index trong lớp CartsController, chúng ta sẽ thấy có nhiều dòng giỏ hàng, đây là trang hiển thị mặc định của Rails mà đến bây giờ chúng ta vẫn chưa chỉnh sửa gì trong này.

capture

Trang này có hiển thị nút ‘Destroy’ để bạn xóa giỏ hàng, tuy nhiên sau khi làm chức năng chỉ xóa giỏ hàng hiện tại ở trên thì bạn chỉ có thể xóa một giỏ hàng trong danh sách đó thôi, mặc dù nếu muốn bạn vẫn có thể bấm nút xóa trên các giỏ hàng khác và Rails vẫn thông báo xóa thành công, nhưng khi load lại trang thì bạn vẫn thấy các giỏ hàng đó tồn tại. Chúng ta sửa lại để hàm này chỉ hiển thị giỏ hàng trong session của trình duyệt như sau:

class CartsController < ApplicationController
    .
    .
    .
    # GET /carts
    # GET /carts.json
    def index  
        @carts = []
        if session[:cart_id] != nil 
            @carts << Cart.find_by_id(session[:cart_id])
        end
    end
    .
    .
    .
end

Vậy là xong, bây giờ nếu bạn mở 2 trình duyệt hoặc mở 2 máy khác nhau, và mỗi trình duyệt đều có giỏ hàng riêng, và cùng trỏ đến trang /carts thì bạn chỉ thấy giỏ hàng của từng trình duyệt đó chứ không thấy giỏ hàng của trình duyệt khác.