12/08/2018, 13:34

Tạo Multistep Forms với gem wicked trong Rails

Chào các bạn mình là Bình, một sinh viên vừa mới tốt nghiệp, hôm nay mình xin chia sẻ với các bạn cách tạo một multistep form với gem wicked trong lập trình Ruby On Rails. Do chỉ là sinh viên mới tốt nghiệp, kinh nghiệm chưa có nhiều, lại là lần đầu viết bài trên viblo nên rất mong nhận được sự ...

Chào các bạn mình là Bình, một sinh viên vừa mới tốt nghiệp, hôm nay mình xin chia sẻ với các bạn cách tạo một multistep form với gem wicked trong lập trình Ruby On Rails. Do chỉ là sinh viên mới tốt nghiệp, kinh nghiệm chưa có nhiều, lại là lần đầu viết bài trên viblo nên rất mong nhận được sự đóng góp của mọi người.

Có lẽ mình sẽ không lan man nữa mà sẽ đi vào chủ đề. Như các bạn biết thì form là một đối tượng rất hay gặp trong lập trình web, thường thì form được sử dụng để tạo hoặc thay đổi dữ liệu. Thông thường việc tạo mới một đối tượng hay sử dụng duy nhất một form để nạp thông tin. Tuy nhiên có một số đối tượng có quá nhiều thông tin, nếu yêu cầu người dùng nhập quá nhiều thông tin trên một form thì đó là một điều không nên trong UX, một cách giải quyết đơn giản là chúng ta hãy chia nhỏ đối tượng đó ra thành nhiều phần nhỏ và cho phép người dùng cập nhật từng phần vào cơ sở dữ liệu. Kỹ thuật đó gọi là tạo multistep form.

Trong Rails có một gem khá hay giúp bạn nhanh chóng tạo được multistep form đó chính là gem wicked.

Bài toán: Giả sử mình có một product với 3 thông tin name, price, category. Giờ mình không muốn nhập cùng một lúc trên cùng một form mà mình muốn nhập name trước sau đó nhập tiếp price rồi cuối cùng nhập category. Lưu ý là name, price và category đều không được bỏ trống.

Khởi tạo project

Mở terminal và tạo một project Rails đơn giản.

rails new myapp

Tạo nhanh một products table

rails g scaffold product name:string price:string category:string

Nhớ đừng quên migrate data.

rake db:migrate

Trong file Gemfile thêm

gem "wicked"

Sau đó bundle install lại

bundle install

Các bước setup dự án coi như đã xong. Giờ tiến hành giải quyết bài toán.

Giải quyết bài toán

Ý tưởng đơn giản của gem wicked là khi tạo mới một product chúng ta sẽ tạo trước một product với dữ liệu trống (chỉ có id được tự sinh ra). Sau đó việc thêm name, price hay category chỉ đơn giản là việc cập nhật product vừa được tạo ra. Chúng ta sẽ validate dữ liệu từng phần, ví dụ phần thêm name thì sẽ chỉ validate cho name...Nào code thôi.

Đầu tiên mình sẽ tạo ra một controller cho các multiple step form.

rails g controller product/steps

Định nghĩa trong router.rb

  resources :products, only: [:new, :create, :index, :destroy] do
    resources :steps, only: [:show, :update], controller: "product/steps"
  end

  root to: "products#index"

Tại model product.rb

Đầu tiên chúng ta sẽ định nghĩa một class attribute mô tả các steps sẽ có. Ở đây mình sẽ có 3 steps là add_name tương ứng với form thêm name vào product, tương tự với price và category.

  cattr_accessor :form_steps do
    %w(add_name add_price add_category)
  end

Định nghĩa một attribute giả để đánh dấu vị trí của step

  attr_accessor :form_step

Validates dữ liệu

  validates :name, presence: true, if: -> { required_for_step?(:add_name) }
  validates :price, presence: true, if: -> { required_for_step?(:add_price) }
  validates :category, presence: true, if: -> { required_for_step?(:add_category) }

  def required_for_step? step
    return true if self.form_steps.index(step.to_s) == self.form_steps.index(form_step)
  end

Ý nghĩa của hàm required_for_step?(step) là so sánh vị trí của form_steps với vị trí attribute cần validate.

Trong steps_controller.rb

Khai báo include Wicked::Wizard và các steps form

class Product::StepsController < ApplicationController
  include Wicked::Wizard
  steps *Product.form_steps

  def show
    @product = Product.find params[:product_id]
    render_wizard
  end

  def update
    @product = Product.find params[:product_id]
    @product.update_attributes product_params(step)
    render_wizard @product
  end

  private
  def product_params step
  	permitted_attributes = case step
                           when "add_name"
                             [:name]
                           when "add_price"
                             [:price]
                           when "add_category"
                             [:category]
                           end
    params.require(:product).permit(permitted_attributes).merge(form_step: step)
  end
end`

Ở đây thì có một vài chú ý là chúng ta sẽ bắt buộc phải khai báo các steps, trong hàm show và update chúng ta lấy product thông qua params[product_id]. Trong params chúng ta sẽ định nghĩa các params được permit theo từng step, thông qua biến step chúng ta có thể biết được chúng ta đang ở form nào. Ứng với mỗi một step sẽ có một view tương ứng. Giả sử chúng ta đang có 3 steps là :add_name, :add_price, :add_category tương ứng trong thư mục views/product/steps sẽ có 3 view add_name.html.erb, add_price.html.erb, add_category.html.erb.

Trong add_name.html.erb

<%= form_for @product, method: :put, url: wizard_path do |f| %>
  <% if @product.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@product.errors.count, "error") %> prohibited this product from being saved:</h2>
      <ul>
        <% @product.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :name %><br>
    <%= f.text_field :name %>
  </div>

  <div class="actions">
    <%= f.submit "Next Step" %>
  </div>
<% end %>

Tương tự với add_price.html.erb, add_category.html.erb.

Tại products_controller.rb, định nghĩa lại hàm create

  def create
    @product = Product.new
    @product.save validate: false
    redirect_to product_step_path(@product, Product.form_steps.first)
  end

Kết luận

Như vậy mình đã kết thúc bài hướng dẫn tạo multi steps form với gem wicked. Tất nhiên trong khuôn khổ của 1 tutorial mình không thể giải thích quá chi tiết tất cả mọi thứ . Để hiểu rõ hơn về gem wicked, mời bạn đọc tại đây. Mã nguồn của mình ở đây.

Ngoài ra còn một vấn đề nữa mình muốn nói thêm đó là khi người dùng tạo product với chỉ id mà không tạo name, price hay category. Như vậy hệ thống của chúng ta sẽ dư thừa dữ liệu trống rất nhiều. Một giải pháp là sử dụng các tác vụ background như whenever, delayed job để xóa các dữ liệu đó.

Nhiệm vụ: Sau khi đọc qua tutorial này bạn hãy thử làm một vài thử thách sau :

1.Tạo trang edit cho product cũng multi steps form.
2.Thử  tạo nút back giữa các form.
3.Thử  tạo multi steps form mà không cần tạo trước object.
4.Thử  tạo multi steps form mà không cần dùng gem.
0