Daily Archives: 25/11/2016

Rails – Hàm render

Hiện tại thì trang /carts/<id> sẽ hiển thị giỏ hàng đang được lưu trong session của trình duyệt người dùng bằng phương thức show trong lớp CartController, phương thức này sẽ “vẽ” những gì được định nghĩa trong file show.html.erb trong thư mục views/carts. 

Trong file này chúng ta “vẽ cứng” cách hiển thị thông tin giỏ hàng 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?'} %>

Tức là đoạn <table>...</table> được code trực tiếp trong file này, trong phần này chúng ta sẽ tách đoạn này ra lưu trong một file khác rồi gọi đến file đó khi cần, bạn có thể hình dung nó giống như là một layout thu nhỏ vậy.

Để có thể gọi đến đoạn code được tách ra thì chúng ta dùng hàm helper có tên là render, hàm này có thể hiển thị một file template (file .html.erb), chuỗi dữ liệu, XML, JSON…v.v

Đầu tiên chúng ta sửa file show.html.erb ở trên như sau:

<%= render @cart %>

Bây giờ file này chỉ có một dòng duy nhất là lời gọi hàm render đến biến @cart, đây là biến lưu trữ thông tin của giỏ hàng được truyền lên từ tham số trong URL /carts/<id>, Rails sẽ tự hiểu là hiển thị đoạn code có trong file _cart.html.erb trong thư mục views/carts, tức là cứ truyền vào một đối tượng thì Rails sẽ gọi đến file có tên dạng _<tên lớp đối tượng>.html.erb (chú ý có dấu gạch dưới), ngoài ra nếu tên đối tượng truyền vào có kí tự ‘s’ ở cuối tên (số nhiều trong tiếng Anh) thì tên file cũng bỏ kí tự này luôn, tức là nếu chúng ta truyền vào @carts thì file này vẫn là _cart.html.erb. Các file như này có tên gọi chung là file partialcác file này Rails không tự tạo nên chúng ta phải tạo bằng tay.

Bây giờ chúng ta tạo file partial _cart.html.erb có nội dung như sau:

<h2>Your cart</h2>
<table>
    <%= render cart.line_items %>
    <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?' } %>

Vẫn là đoạn code cũ, chỉ có khác một chỗ là đoạn hiển thị danh sách các sản phẩm cùng với số lượng và tổng tiền của chúng thì chúng ta lại gọi một hàm render đến một file partial khác, ở đây chúng ta truyền vào biến cart.line_items. Khi biến @cart được truyền vào file _cart.html.erb, chúng ta tham chiếu đến biến này với cái tên trùng với file partial này, tức là _cart.html.erb thì chúng ta tham chiếu tới biến cart, không có dấu '@' nữa.

Và ở đây chúng ta truyền vào hàm render đối tượng cart.line_items, đây là một đối tượng lưu trữ dạng danh sách/tập hợp nhiều phần tử, cũng tương tự như trên, chỉ khác là Rails sẽ lặp qua danh sách đó, với mỗi lần lặp thì Rails lại gọi file partial tương ứng là _line_item.html.erb. Do đây là các đối tượng thuộc lớp LineItem nên file partial cũng nằm trong thư mục line_items cho dù có hàm gọi nó ở bên thư mục khác.

Bây giờ chúng ta tạo file _line_item.html.erb trong thư mục app/views/line_items như sau:

<tr>
    <td><%= line_item.quantity%>x</td>
    <td><%= line_item.product.title %></td>
    <td class="item_price"><%= number_to_currency(line_item.total_price) %></td>
</tr>

Chúng ta hiển thị số tiền, tên sản phẩm và tổng tiền như bình thường.

Bạn có thể chạy lại và thấy giao diện vẫn hiển thị như cũ, chỉ khác là bây giờ các phần được hiển thị ở các file khác nhau chứ không được code “cứng” trong một file nữa.

aacapture

Và để tận dụng khả năng phân tách này thì chúng ta hiển thị giỏ hàng ở bên sidebar của layout luôn. Chúng ta sửa lại file application.html.erb như sau:

<!DOCTYPE html>
<html>
<head>
<title>Books Store</title>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
    <%= csrf_meta_tags %>
</head>
<body id="store">
    <div id="banner">
        <%= image_tag("logo.png") %>
        <%= @page_title || "Books Store" %>
    </div>
    <div id="columns">
        <div id="side">
            <div id="cart">
                <%= render @cart %>
            </div>

            <a href="#">Home</a>
            <a href="#">FAQ</a>
            <a href="#">News</a>
            <a href="#">Contact</a>
        </div>
        <div id="main">
            <%= yield %>
        </div>
    </div>
</body>
</html>

Ở đây chúng ta cũng gọi hàm render với tham số là biến @cart để hiển thị từ file _cart.html.erb. Tuy nhiên khác với lời gọi từ file show.html.erb là có kèm theo tham số sau URL là /carts/<id>, ở đây lời gọi tại file application.html.erb sẽ gọi phương thức index trong lớp StoreController, phương thức này đã được cấu hình để nhận URL '/' nếu bạn còn nhớ, và phương thức này không nhận tham số nào cả, do đó trong phương thức này chúng ta phải tạo một đối tượng @cart để sử dụng, chúng ta sửa lại file này như sau:

class StoreController < ApplicationController
    def index
        @products = Product.all
        @cart = current_cart
    end
end

Tiếp theo chúng ta định nghĩa thêm một vài lớp CSS:

.
.
.
/* Cart sidebar Style */
#cart, #cart table {
    font-size: smaller;
    color: white;
} 

#cart table {
    border-top: 1px dotted #595;
    border-bottom: 1px dotted #595;
    margin-bottom: 10px;
}

Bây giờ mỗi khi bấm nút ‘Add to Cart’ trên sản phẩm thì chúng ta sẽ được trỏ về trang /line_items?id=<id>, chúng ta sửa lại phương thức create trong lớp LineItemsController để hàm này trỏ lại trang '/' như sau:

class LineItemsController < ApplicationController
    .
    .
    .
    # POST /line_items
    # POST /line_items.json
    def create
        @cart = current_cart
        product = Product.find(params[:product_id]) 
        @line_item = @cart.add_product(product.id)
 
        respond_to do |format|
            if @line_item.save
                format.html { redirect_to('/', :notice => 'Line item was successfully created') } 
                format.json { render :show, status: :created, location: @line_item }
            else
                format.html { render :new }
                format.json { render json: @line_item.errors, status: :unprocessable_entity }
            end
        end
    end
    .
    .
    .
end

Vậy là xong, bây giờ website của chúng ta sẽ có giao diện như thế này:

bbbbcapture

Tuy nhiên còn một vấn dề nhỏ nữa, nếu bạn trỏ vào các trang như /carts, /products, /line_items thì bạn sẽ bị lỗi biến @cart dùng để gọi render bên sidebar không tồn tại (có giá trị nil).

capture

Lý do là vì chúng ta chỉ định nghĩa biến này ở hàm index trong lớp StoreController thôi, để sửa thì bạn có thể vào từng phương thức index của từng lớp CartsController, ProductsControllerLineItemsController và chèn thêm dòng khai báo @cart = current_cart là sẽ hết lỗi, tuy nhiên cách này rất “cùi bắp” vì cứ mỗi lần tạo thêm controller thì chúng ta lại phải thêm một dòng như thế.

Do đó có một cách khác là chúng ta khai báo phương thức current_cart là một phương thức helper, tức là phương thức này sẽ có thể gọi được từ các file View, để làm điều này thì chúng ta thêm dòng khai báo sau phương thức current_cart như sau:

class ApplicationController < ActionController::Base
    # Prevent CSRF attacks by raising an exception.
    # For APIs, you may want to use :null_session instead.
    protect_from_forgery with: :exception
 
    private
 
    def current_cart
        Cart.find(session[:cart_id])
    rescue
        cart = Cart.create
        session[:cart_id] = cart.id
        cart
    end
 
    helper_method :current_cart
end

Phương thức helper_method nhận vào tên các phương thức (cách nhau bởi dấu phẩy), phương thức nào được truyền vào thì sẽ được định nghĩa là một phương thức helper.

Và chúng ta có thể gọi đến phương thức này trong file View, chúng ta sửa lại file application.html.erb như sau:

<!DOCTYPE html>
<html>
<head>
<title>Books Store</title>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
    <%= csrf_meta_tags %>
</head>
<body id="store">
    <div id="banner">
        <%= image_tag("logo.png") %>
        <%= @page_title || "Books Store" %>
    </div>
    <div id="columns">
        <div id="side">
            <div id="cart">
                <%= render current_cart %>
            </div>

            <a href="#">Home</a>
            <a href="#">FAQ</a>
            <a href="#">News</a>
            <a href="#">Contact</a>
        </div>
        <div id="main">
            <%= yield %>
        </div>
    </div>
</body>
</html>

Vậy là hết lỗi! Dòng @cart = current_cart trong phương thức index của lớp StoreController bây giờ trở nên vô nghĩa, bạn có thể bỏ đi nếu muốn.