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 partial,
cá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.
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:
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
).
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, ProductsController
và LineItemsController
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.