0

Multistep Forms

1. Giới thiệu bài toán:

  • Chào mọi người, là một Web Developer thì chúng ta đã quá quen việc làm việc với form, trong đa số trường hợp thì các field thường được trình bày 1 cách liên tục như ví dụ dưới đây:

  • Tuy nhiên trong 1 số trường hợp (ví dụ form quá nhiều field hoặc do yêu cầu về business login) form được chia thành nhiều step như ví dụ dưới đây:

Hôm nay mình xin phép giới thiệu với mn cách mình thực hiện sử dụng jQueryAjax.

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

a. Init project và scaffold:

  • Lấn lượt chạy các command sau để khởi tạo project và generate scaffold

    rails new rails_multi_steps_form
    rails generate scaffold shipping receiver_name:string receiver_phone:string shipping_address:string shipping_day:datetime
    
  • Thêm các gem jquery-railsbootstrap-sass vào Gemfile.

    gem "jquery-rails"
    gem "bootstrap-sass"
    
  • Chạy comamnd bundle install và thêm các config cần thiết để có thể sử dụng gem jquery-railsbootstrap-sass

    bundle install
    
  • Sử dụng gem jquery-rails và gem bootstrap-sass và sửa lại các file được scaffold generate sẵn để có màn hình tạo shipping như mong muốn.

  • Các file liên quan:

    # app/views/shippings/new.html.erb
    
    <div class="container">
      <h1>Create Shipping</h1>
    
      <%= render "form", shipping: @shipping %>
    </div>
    
    # app/views/shippings/form.html.erb
    
    <%= form_for shipping, html: { class: "form-horizontal" } do |form| %>
      <%= render "shared/error_messages", object: shipping if shipping.errors.any? %>
    
      <div class="form-group">
        <%= form.label :receiver_name, class: "control-label col-sm-2" %>
        <div class="col-sm-10">
          <%= form.text_field :receiver_name, class: "form-control" %>
        </div>
      </div>
    
      <div class="form-group">
        <%= form.label :receiver_phone, class: "control-label col-sm-2" %>
        <div class="col-sm-10">
          <%= form.text_field :receiver_phone, class: "form-control" %>
        </div>
      </div>
    
      <div class="form-group">
        <%= form.label :shipping_address , class: "control-label col-sm-2" %>
        <div class="col-sm-10">
          <%= form.text_field :shipping_address, class: "form-control" %>
        </div>
      </div>
    
      <div class="form-group">
        <%= form.label :shipping_day, class: "control-label col-sm-2" %>
        <div class="col-sm-10">
          <%= form.datetime_field :shipping_day, class: "form-control" %>
        </div>
      </div>
    
      <div class="form-group">
        <div class="col-sm-offset-2 col-sm-10">
          <%= form.submit class: "btn btn-primary" %>
        </div>
      </div>
    <% end %>
    
    # app/vires/shared/_error_messages.html.erb
    
    <div class="panel panel-danger">
      <div class="panel-heading">
        Please try again
      </div>
    
      <div class="panel-body">
        <ul>
          <% object.errors.full_messages.each do |message| %>
            <li><%= message %></li>
          <% end %>
        </ul>
      </div>
    </div>
    
  • Kết quả thu được

b. Chia form thành multi step form:

  • Tạo các file ứng với các step của form

  • Chỉ hiển thị step đầu tiên, ẩn các bước tiếp theo (sử dụng style="display: none;")

  • Step confirmation chỉ được hiển thị sau khi đã thực hiện điền các field vào các step.

    # app/views/shippings/_form.html.erb
    
    <%= form_for shipping, html: { class: "form-horizontal" } do |form| %>
      <div class="js-form-step">
        <%= render "shippings/form_steps/receiver_step", form: form, shipping: shipping %>
      </div>
    
      <div class="js-form-step" style="display: none">
        <%= render "shippings/form_steps/shipping_step", form: form, shipping: shipping %>
      </div>
    <% end %>
    
    # app/views/shippings/form_steps/_receiver_step.html.erb
    
    <h3>Receiver Step Step</h3>
    
    <%= render "shared/error_messages", object: shipping if shipping.errors.any? %>
    
    <div class="form-group">
      <%= form.label :receiver_name, class: "control-label col-sm-2" %>
      <div class="col-sm-10">
        <%= form.text_field :receiver_name, class: "form-control" %>
      </div>
    </div>
    
    <div class="form-group">
      <%= form.label :receiver_phone, class: "control-label col-sm-2" %>
      <div class="col-sm-10">
        <%= form.text_field :receiver_phone, class: "form-control" %>
      </div>
    </div>
    
    <div class="form-group">
      <div class="col-sm-offset-2 col-sm-10">
        <div class="col-xs-6 text-left">
          <div class="previous">
            <button class="btn btn-primary" disabled>
              Previous Step
            </button>
          </div>
        </div>
    
        <div class="col-xs-6 text-right">   
          <div class="next">
            <button class="btn btn-primary">
              Next Step
            </button>
          </div>
        </div>
      </div>
    </div>
    
    # app/views/shippings/form_steps/_shipping_step.html.erb
    
    <h3>Shipping Step</h3>
    
    <%= render "shared/error_messages", object: shipping if shipping.errors.any? %>
    
    <div class="form-group">
      <%= form.label :shipping_address , class: "control-label col-sm-2" %>
      <div class="col-sm-10">
        <%= form.text_field :shipping_address, class: "form-control" %>
      </div>
    </div>
    
    <div class="form-group">
      <%= form.label :shipping_day, class: "control-label col-sm-2" %>
      <div class="col-sm-10">
        <%= form.datetime_field :shipping_day, class: "form-control" %>
      </div>
    </div>
    
    <div class="form-group">
      <div class="col-sm-offset-2 col-sm-10">
        <div class="col-xs-6 text-left">
          <div class="previous">
            <button class="btn btn-primary">
              Previous Step
            </button>
          </div>
        </div>
    
        <div class="col-xs-6 text-right">   
          <div class="next">
            <button class="btn btn-primary">
              Next Step
            </button>
          </div>
        </div>
      </div>
    </div>
    
    # app/views/shippings/form_steps/_confirmation_step.html.erb
    
    <h3>Confirmation Step</h3>
    
    <%= render "shared/error_messages", object: shipping if shipping.errors.any? %>
    
    <div class="form-group">
      <div class="col-sm-offset-2 col-sm-10">
        <table class="table">
          <tbody>
            <tr>
              <th>Receiver Name</th>
              <td><%= "" %></td>
            </tr>
            <tr>
              <th>Receiver Phone</th>
              <td><%= "" %></td>
            </tr>
            <tr>
              <th>Shipping Address</th>
              <td><%= "" %></td>
            </tr>
            <tr>
              <th>Shipping Day</th>
              <td><%= "" %></td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
    
    <div class="form-group">
      <div class="col-sm-offset-2 col-sm-10">
        <div class="col-xs-6 text-left">
          <div class="previous">
            <button class="btn btn-primary">
              Previous Step
            </button>
          </div>
        </div>
    
        <div class="col-xs-6 text-right">   
          <div class="next">
            <%= form.submit "Done", class: "btn btn-primary" %>
          </div>
        </div>
      </div>
    </div>
    
  • Kết quả thu được:

c. Implement click event cho pre button và next button:

  • Thêm class .js-button-pre.js-button-nextdata-step-index cho pre button và next button
  • Step index lần lượt là 0 ứng với receiver step, 1 ứng với shipping step và 2 ứng với confirmation step
  • Ví dụ ở shipping step:
    <button class="btn btn-primary js-button-pre" data-step-index="1">
      Previous Step
    </button>
    
    <button class="btn btn-primary js-button-next" data-step-index="1">
      Next Step
    </button>
    
  • Implement click event cho các .js-button-pre.js-next-button như sau
    # app/assets/javascripts/application.js
    
    $( document ).on('turbolinks:load', function() {
      $(document).on('click', '.js-button-next', function() {
        var stepIndex = $(this).data('stepIndex');
        $('.js-form-step').slideUp();
        $('.js-form-step').eq(stepIndex + 1).slideDown();
      });
    
      $(document).on('click', '.js-button-pre', function() {
        var stepIndex = $(this).data('stepIndex');
        $('.js-form-step').slideUp();
        $('.js-form-step').eq(stepIndex - 1).slideDown();
      });
    });
    
  • Kết quả thu được:

c. Validate ở từng step

  • Ở bước này, ta sẽ thực hiện gửi giá từng field được nhập ở từng step lên server và thực hiện validate những giá trị này có valid hay không.

  • Ta thêm action validate_step cho ShippingsController để thực hiện việc này.

    # config/routes.rb
    
    resources :shippings do
      collection do
        post :validate_step
      end
    end
    
    # app/controllers/shippings_controller.rb
    
    def validate_step
      shipping = Shipping.new shipping_params
      shipping.valid?
    
      error_attrs = shipping_params.keys.map(&:to_sym).select do |attr|
        shipping.errors[attr].any?
      end
      error_messages = error_attrs.map do |attr|
        Shipping.human_attribute_name(attr) + " " + shipping.errors[attr].first
      end
    
      respond_to do |format|
        format.js do
          render json: {
            valid: error_messages.empty?,
            error_messages: render_to_string(
              partial: "shared/error_messages",
              locals: { error_messages: error_messages }
            )
          }
        end
      end
    end
    
  • Update partial shared/_error_messages.html.erb

    <div class="panel panel-danger">
      <div class="panel-heading">
        Please try again
      </div>
    
      <div class="panel-body">
        <ul>
          <% error_messages.each do |message| %>
            <li><%= message %></li>
          <% end %>
        </ul>
      </div>
    </div>
    
  • Update click event của .js-button-next, sử dụng Ajax để gọi action validate_step

    $(document).on('click', '.js-button-next', function() {
        var form = $(this).closest('.js-form-step');
        var stepIndex = $(this).data('stepIndex');
        var data = {};
    
        form.find('input').each(function(index) {
          var name = $(this).attr('name');
          var val = $(this).val();
    
          data[name] = val;
        });
    
        $.ajax({
          headers: {
            'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
          },
          url: '/shippings/validate_step',
          method: 'POST',
          dataType: 'JSON',
          data: data,
          success: function(response) {
            if (response.valid) {
              $('.js-form-step').slideUp();
              $('.js-form-step').eq(stepIndex + 1).slideDown();
    
              form.find('.js-error-messages').html("");
            } else {
              form.find('.js-error-messages').html(response.error_messages);
            }
          }
        });
      });
    
  • Kết quả thu được

d. Render confirmation step:

  • Update response action validate_step trả về thêm response html của confirmation_step
    # app/controllers/shippings_controller.rb
    
    def validate_step
      shipping = Shipping.new shipping_params
      shipping.valid?
    
      error_attrs = shipping_params_keys.select do |attr|
        shipping.errors[attr].any?
      end
      error_messages = error_attrs.map do |attr|
       Shipping.human_attribute_name(attr) + " " + shipping.errors[attr].first
      end
    
      respond_to do |format|
        format.js do
          render json: {
            valid: error_messages.empty?,
            error_messages: render_to_string(
              partial: "shared/error_messages",
              locals: { error_messages: error_messages }
            ),
            confirmation: render_to_string(
              partial: "shippings/form_steps/confirmation_step",
              locals: { shipping: shipping }
            ) 
          }
        end
      end
    end
    
  • Update click event của .js-button-next, render confirmation_step nếu step hợp lệ và đang ở step cuối.
    $(document).on('click', '.js-button-next', function() {
      var form = $(this).closest('.js-form-step');
      var stepIndex = $(this).data('stepIndex');
      var data = $('form').serialize();
    
      $.ajax({
        headers: {
          'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
        },
        url: '/shippings/validate_step',
        method: 'POST',
        dataType: 'JSON',
        data: data,
        success: function(response) {
          if (response.valid) {
            $('.js-form-step').slideUp();
            $('.js-form-step').eq(stepIndex + 1).slideDown();
            $('input[name="step_index"]').val(stepIndex + 1);
            form.find('.js-error-messages').html("");
    
            if (stepIndex == 1) {
              $('.js-form-step').last().html(response.confirmation);
            }
          } else {
            form.find('.js-error-messages').html(response.error_messages);
          }
        }
      });
    });
    
  • Kết quả thu được:

3. Tham khảo:

  • Các bạn có thể download source code ở đây.
  • Các bạn cũng có thể sử dụng gem wicked để handel multi step form.

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí