12/08/2018, 14:02

Thao tác với Forms trong Rails 4

Khi phát triển ứng dụng web nói chung và với Rails nói riêng, có thể có khi chúng ta gặp trường hợp khi chúng ta dùng nested form với @post has_many comments mà chúng ta cần submit lưu các comments tại 1 tab sau đó submit tab form @post chính thì cần lưu các comments vừa save thuộc vào @post. Chú ý ...

Khi phát triển ứng dụng web nói chung và với Rails nói riêng, có thể có khi chúng ta gặp trường hợp khi chúng ta dùng nested form với @post has_many comments mà chúng ta cần submit lưu các comments tại 1 tab sau đó submit tab form @post chính thì cần lưu các comments vừa save thuộc vào @post. Chú ý là chỉ có tab chính của form @post mới submit theo dạng html hoặc ajax, tất cả các tabs còn lại đều submit dùng JS. (bởi vì chỉ khi submit form chính để tạo post, /posts/new).

  • Submit từng tab, sau đó submit tab form chính để lưu post.

Cuối cùng sau khi submit thì chúng ta cần show ra post và các comments của nó như khi đã điền vào form.

Mô hình CSDL

Để tiện theo dõi mình sẽ tạo mô hình như đã nêu phía trên là Post và Comment.

Khởi tạo mô hình

Tạo model Post.

$ rails g scaffold post title:string content:text

Tạo model Comment

$ rails g scaffold comment content:text

Migrate database

$ rake db:migrate

Thiết lập mối quan hệ

Ở đây có duy nhất một mối quan hệ là Post có nhiều Comments. Vì vậy mình sử dụng nested-form cho việc tạo form.

Mình dùng gem cocoon cho việc build form nested này. Có thể dùng gem nested-form nhưng mình dùng gem cocoon vì nó mới và nhẹ hơn.

Bài viết này mình không đi vào việc cài đặt và sử dụng gem kia như thế nào. Mặc định là đã dùng được rồi nhé.

Chỉnh sửa chút về Post.rb

class Post < ActiveRecord::Base
  has_many :comments

  accepts_nested_attributes_for :comments, allow_destroy: true,
    reject_if: :all_blank
end

Với model Comment

class Comment < ActiveRecord::Base
  belongs_to :post
end

Như vậy là mình đã cài đặt xong quan hệ. Phần tiếp theo mình sẽ trình bày về xây dựng trang posts/new với 2 tabs gồm tab chính chứa form @post, tab bên là nested-form cho @post - comments.

Tiếp theo với chủ đề này. Ở phần đầu mình đã nêu ra vấn đề và thiết lập mô hình.

Bài viết này mình sẽ trình bày về tạo form. Từng bước một để mọi người tiện theo dõi.

Tạo trang new post với 2 tabs

Ở đây mình dùng bootstrap.

Đầu tiên cần thêm vào route.rb

  resources :posts
  resources :comments

Mình sẽ viết theo cú pháp slim-rails cho code ruby.

Tiếp theo mở controllers/posts_controller.rb

class PostsController < ApplicationController
  def new
    @post = Post.new
  end
end

Mở views/posts/new.slim

.tabs-container
  ul.nav.nav-tabs
    li.active
      a aria-expanded="true" data-toggle="tab" href="#basic-information"
        = t(".basic_information")
    li
      a aria-expanded="false" data-toggle="tab" href="#comments"
        = t(".comments")

  .tab-content
    #basic-information.tab-pane.active
      .panel-body
        = render "form", post: @post
    #comments.tab-pane
      .panel-body
        = render partial: "comments_form",
          locals: {post: @post}

Ở phía trên mình tạo ra 2 tabs, một tab đầu tiên là chứa form chính cho điền các thông tin về post, tab thứ hai là chứa form để điền thông tin về các comments cho post.

Tạo views/posts/_form.slim

= simple_form_for post, html: {class: "form-horizontal"} do |f|
  = f.input :title, label: t(".title")
  = f.input :content, label: t(".content")

  = f.submit t(".save"), class: "btn btn-w-m btn-primary"

Tạo views/posts/_comments_form.slim

#comments-form
  = simple_form_for post do |f|
    = f.hidden_field :id
    #comments
      = f.fields_for :comments do |comment|
        = render partial: "shared/comment_fields",
          locals: {f: comment}

      .links.text-center.margin-top-10
        = link_to_add_association f, :comments, partial: "shared/comment_fields",
          class: "btn btn-default" do
          span.fa.fa-plus
          =< t(".add_sample")

    .text-center.margin-top-20
      = link_to "#!", class: "btn btn-w-m btn-primary",
        data: {disable_with: t(".submitting")} do
        = t(".save")

Như vậy mình đã xây dựng 2 forms cho 2 tabs tại trang posts/new. Ở đây mỗi tab là một form với chung một object là @post. và đều có nút submit riêng.

Ở phần tiếp theo mình sẽ trình bày tiếp về phần submit riêng tab comments_form trước để lưu trữ các comments nhưng chưa thuộc vào post nào cả, sau khi bấm submit tại form post chính thì mới lưu post và liên kết các comments đã tạo ở tab bên kia vào post này.

Và không load lại trang khi submit tạo comments. Chỉ redirect_to post_path @post khi submit lưu post tại form chính.

Tiếp tục với phần bài viết về submit multiple tabs form, phần này mình trình bày về chức năng submit lần lượt từng tab, sử dụng ajax để lưu cho các tab phụ và submit sử dụng html cho tab form chính.

Ở phần trước mình đã trình bày về tạo form cho 2 tabs là form post và comment form.

Vì chúng ta sẽ sử dụng ajax để submit form lưu comments, khi này comments được lưu nhưng chưa thuộc về post nào cả. Sau khi tạo xong comments người dùng submit form post thì bắt đầu lưu post và liên kết các comments đã tạo ở form kia vào trong post mới tạo này.

Nhìn lại relation ta có

class Post < ActiveRecord::Base
  has_many :comments

  accepts_nested_attributes_for :comments, allow_destroy: true,
    reject_if: :all_blank
end
class Comment < ActiveRecord::Base
  belongs_to :post
end

Vậy để liên kết các comments đã có vào trong post khi tạo chúng ta sẽ thêm chút sau vào controller.

# posts_controller.rb

class PostsController < ApplicationController
  def new
    @post = Post.new post_params
  end

  def create
    @post = Post.create post_params
    redirect_to post_path @post
  end

  private
  def post_params
    params.require(:post).permit :title, :content, comment_ids: []
  end
end

Sử dụng params chứa comment_ids: [] là một array chứa các ID của comments đã tạo ở tab bên kia nhằm mục đích liên kết nó vào post khi tạo ở tab này.

Tiếp theo chúng ta đến với việc tạo các comments ở tab comment_form.

Vì đây chúng ta tạo nhiều comments một lúc khi bấm nút save, do vậy mình sẽ không sử dụng comments_controller.rb (bởi vì controller này hiểu rằng dùng cho việc CRUD cho 1 comment.

Do vậy ở đây mình có thể sử dụng resource thay vì dùng resources. Ví dụ mình sử dụng một controller khác với resource như sau:

# routes.rb

resource :batch_comments

Về một số khác biệt giữa resource số ít và resources số nhiều mình có viết ở bài dưới đây.

Tại đây chúng ta sẽ gửi request lên controller vừa tạo là batch_comments_controler.rb và xử lý tại controller này.

Tuy nhiên cũng có một giải pháp khác để dễ dàng bảo trì hơn khi chúng ta làm với dự án lớn đó là sử dụng services. Khi này chúng ta sẽ không xử lý logic nhiều ở controller nữa mà sẽ xử lý nó ở trong 1 service và trả kết quả cho controller.

Mình sẽ viết một bài về services sau. Với phần này mình sẽ xử lý hết ở controller.

Đầu tiên chúng ta tạo batch_comments_controller.rb như sau:

  rails g controller batch_comments

Đi tới xử lý tạo các comments tại controller này.

Sửa một chút về form_comment như sau:

# _comment_form.slim

= simple_form_for post, url: batch_comments_path, method: :post do |f|
  = f.hidden_field :id
  ...
  = link_to "#!", class: "btn btn-w-m btn-primary", id: "js-comment-form-save-btn",
    data: {disable_with: t(".submitting")} do
    = t(".save")

Tại batch_comments.coffee chúng ta xử lý sử dụng ajax để submit form và xử lý sau khi comments được lưu như sau:

$ ->
  $("#js-comment-form-save-btn").click ->
    is_disabled = $("#js-comment-form-save-btn").attr "disabled"
    return if is_disabled
    id = $("#post_id").val()
    params = $("#comments-form form").serialize()
    submitting_text = $(@).data "disable-with"
    submitted_text = $(@).html()
    $("#js-comment-form-save-btn").attr "disabled", true
    $("#js-comment-form-save-btn").html submitting_text

    $.ajax
      type: "POST"
      dataType: "JSON"
      url: "/batch_comments"
      data: params
      success: (data) ->
        $("#js-comment-form-save-btn").html submitted_text
        $("#js-comment-form-save-btn").removeAttr "disabled"
        insert_comment_ids_to_forms data
      error: () ->
        $("#js-comment-form-save-btn").html submitted_text
        $("#js-comment-form-save-btn").removeAttr "disabled"

  return

insert_comment_ids_to_forms = (data) ->
  $.each data, (key, value) ->
    comment_field_comment_form = "<input value="#{value}" name="post[comments_attributes][#{key}][id]" type="hidden">"
    comment_field_basic_form = "<input value="#{value}" name="post[comment_ids][]" type="hidden">"
    $("#comments-form form").append comment_field_comment_form
    $("#basic-information form").append comment_field_basic_form

Tiếp theo chúng ta xử lý trên server như sau:

# batch_comments_controller.rb

class BatchCommentsController < ApplicationController
  def create
    @result =
      begin
        if params[:id].present?
          post = Post.find params[:id]
          post.assign_attributes params
          post.save!
          {success: true}
        else
          ActiveRecord::Base.transaction do
            result = {}
            params[:comments_attributes]&.each do |key, value|
              if value[:id].present?
                Comment.find(value[:id]).update value
              else
                result[key] = Comment.create!(value).id
              end
            end
            result
          end
        end
      rescue StandardError
        {success: false}
      end
    render nothing: true, status: 400 if @result == {success: false}
  end

  private
  def post_params
    params.require(:post).permit :title, :content, comment_ids: []
  end
end

Cuối cùng mình tạo file builder trong view để render kết quả.

# views/batch_comments/create.json.jbuilder
json.merge!(@result)

Như vậy là đã xong phần submit form comment sử dụng ajax.

Cuối cùng để lưu bài post chúng ta submit post_form như thông thường. Nó sẽ tiến hành lưu bài post cùng với các comments mình đã tạo ở bên tab kia.

Cảm ơn các bạn đã xem bài viết.

0