Nested form Rails với gem Cocoon


Hôm nay tôi xin giới thiệu đến các bạn một công cụ của Rails giúp bạn xử lý form nested đơn giản hơn. Công cụ tôi đang muốn nhắc đến là Cocoon. Vậy Cocoon có thể làm được gì giúp chúng ta? Hãy cùng bắt đầu dùng thử Cocoon nhé!

Điều kiện tiên quyết
Gem này phụ thuộc vào jQuery, do đó sẽ tốt hơn nếu trong project của bạn đã sử dụng jQuery
Cài đặt (Ở đây tôi chỉ giới thiệu với Rails phiên bản >= 4) Add vào Gemfile
gem "cocoon"
Chú ý, ở Rails 4.x thì phiên bản cocoon tối thiểu là v1.2.0 hoặc mới hơn.
Trong application.js bạn include vào
//= require cocoon
Đừng quên chạy lệnh bundle để hoàn tất cài đặt gói gem vào project.
Include cacoon vào project bằng cách thêm vào application.js
//= require cocoon
Cách sử dụng
Giả sử ta có model Project:
rails g scaffold Project name:string description:string
và project có nhiều Task
rails g model Task description:string done:boolean project:belongs_to
quan hệ trong model được định nghĩa:
Ruby
class Project < ActiveRecord::Base
  has_many :tasks
  accepts_nested_attributes_for :tasks, reject_if: :all_blank, allow_destroy: true
end
Ruby
class Task < ActiveRecord::Base
  belongs_to :project
end
Tiếp theo ta định nghĩa một file có tên _task_field.html.erb trong show (views/periods)
Strong Paramasters
Strong Params của projects accepts_nested các params của tasks
Ruby
 def project_params
    params.require(:project).permit(:name, :description,
        tasks_attributes: [:id, :description, :done, :_destroy])
  end
Cấu hình mặc định Cocoon yêu cầu link_to_add_association và liên quan giữa các phần tử.
Dưới đây là một ví dụ layouts đơn giản
định nghĩa trong views/projects/_form (ở đây _form được định dạng haml )
Default
= semantic_form_for @project do |f|
  = f.inputs do
    = f.input :name
    = f.input :description
    %h3 Tasks
    #tasks
      = f.semantic_fields_for :tasks do |task|
        = render 'task_fields', f: task
      .links
        = link_to_add_association 'add task', f, :tasks
    = f.actions do
      = f.action :submit
trong _form có render đến _task_fields:
Default
.nested-fields
  = f.inputs do
    = f.input :description
    = f.input :done, as: :boolean
    = link_to_remove_association "remove task", f
Form dưới dạng đơn giản hơn ta có thể viết
projects/_form
Default
= simple_form_for @project do |f|
  = f.input :name
  = f.input :description
  %h3 Tasks
  #tasks
    = f.simple_fields_for :tasks do |task|
      = render 'task_fields', f: task
    .links
      = link_to_add_association 'add task', f, :tasks
  = f.submit
_task_fields
Default
 .nested-fields
  = f.input :description
  = f.input :done, as: :boolean
  = link_to_remove_association "remove task", f
Dưới dạng form chuẩn
projects/_form
Default
 = form_for @project do |f|
  .field
    = f.label :name
    %br
    = f.text_field :name
  .field
    = f.label :description
    %br
    = f.text_field :description
  %h3 Tasks
  #tasks
    = f.fields_for :tasks do |task|
      = render 'task_fields', f: task
    .links
      = link_to_add_association 'add task', f, :tasks
  = f.submit
_task_fields
Default
.nested-fields
  .field
    = f.label :description
    %br
    = f.text_field :description
  .field
    = f.check_box :done
    = f.label :done
  = link_to_remove_association "remove task", f
Cocoon hoạt động thế nào
Cocoon định nghĩa hai helper functions:
link_to_add_association
function này cho biết thêm một liên kết để đánh dấu của bạn rằng, khi click vào, tự động thêm một partial form liên quan. Ta có thể gọi đó là builder form.
link_to_add_association có 4 tham số đầu vào:
name: nội dung text hiển thị trên link
f: form builder
association: tên của một association( số nhiều), có thể là một new instance cần được add vào (có thể là một text, một string)
html_options: phần mở rộng các thành phần html, tương tự như phần mở rộng của link_to dưới đây là một số options mở rộng đặc biệt, ba cái đầu tiên cho phép kiểm soát vị trí của các new link-data:
data-association-insertion-traversal: phương pháp jquery cho phép nút chọn tương đối so với các linkclosestnextchildren, mặc định sẽ là tuyệt đối.
data-association-insertion-node: jquery selector của node như là string, hoặc là một function mang theo nút link_to_add_association như là một paramaster và return một nút. Mặc định là nút parent.
data-association-insertion-method : method jquery insert vào dữ liệu mới. beforeafterappend,prepend... mặc định là: before
data-association-insertion-position một method cũ định nghĩa data được định nghĩa vào đâu.
settings này vẫn hoạt động, tuy nhiên data-association-insertion-position có quyền ưu tiên, do đó nên remove method này khỏi các versions trong tương lai.
partial: khai báo rõ ràng tên của partial sẽ sử dụng.
render_options: các options thông qua form-builder function ( ví dụ: simple_fields_for,semantic_fields_for hay là fields_for). Nếu có chứa tùy chọn :locals trong hash, nó sẽ được xử lý trong phần partial.
wrap_object: một proc sẽ bao ngoài object của bạn, đặc biệt hữu ích nếu bạn đang dùng decorators.
force_non_association_create: nếu true, sẽ không tạo object sử dụng association.
form_name: tên của form paramaster trong partial, mặc định sẽ là f.
Một cách tùy ý, bạn có thể bỏ qua tên và thiết lập một block các option cho các thẻ link.
:render_options
Bên trong html_options ta có thể thêm vào :render_options, và trong hash sẽ truyền đến cho form buidler để insert form.
Khi sử dụng Bootstrap và simple form cùng nhau, simple_fields_for cần có option wrapper: 'inline' và sẽ được xử lý như sau:
Default
= link_to_add_association 'add something', f, :something,
    render_options: { wrapper: 'inline' }
để xác định locals cần thiết để xử lý khi truyền vào partial:
Default
= link_to_add_association 'add something', f, :something,
    render_options: {locals: { sherlock: 'Holmes' }}
:partial
Để overide lại tên partial, bởi vì có partial được dùng chung bởi nhiều views, ta đặt ở thư mục ngoài views (shared chẳng hạn):
Default
= link_to_add_association 'add something', f, :something,
  partial: 'shared/something_fields'
:wrap_object
wrap_object được xử lý như thế nào? Nếu bạn đang sử dụng decorator, các biến instance bình thường của các object liên quan là không đủ dùng, bạn nhất thiết phải tạo một decorated object.
Một decorator ví dụ:
Default
class CommentDecorator
  def initialize(comment)
    @comment = comment
  end

  def formatted_created_at
    @comment.created_at.to_formatted_s(:short)
  end

  def method_missing(method_sym, *args)
    if @comment.respond_to?(method_sym)
      @comment.send(method_sym, *args)
    else
      super
    end
  end
end
để sử dụng decorator đó:
Default
= link_to_add_association('add something', @form_obj, :comments,
    wrap_object: Proc.new {|commen CommentDecorator.new(comment) })
:force_non_association_create
Trong trường hợp bình thường, chúng ta tạo ra một đối tượng lồng nhau liên kết chính nó. Đây là cách clear nhất để tạo ra một đối tượng một đối tượng lồng.
Cách sử dụng này có một tác dụng phụ, với mỗi lần gọi link_to_add_association, một phần tử mới được thêm vào liên kết. Đây không phải là một trượng hợp dài hơn.
Với khả năng tương thích ngược chúng ta sẽ giữ lại các options. Hoặc vì một lý do nào đó mà bạn muốn tạo ra một đối tượng không dựa trên sự liên kết.
Default
 = link_to_add_association('add something', @form_obj, :comments,
    force_non_association_create: true)
Mặc định sẽ là: :force_non_association_create là false.
link_to_remove_association
Chức năng này sẽ thêm một đánh dấu của bạn rằng, khi click, tự động remove partial form xung quanh. Vị trí của nó nên ở bên trong __fields
Chúng ta có 3 paramaster:
name: text show ra ở link
f: truy cập đến nơi chứa form-object html_options: phần mở rộng html-options (có thể tham khảo link_to)
Bạn cũng có thể replace name bằng một block có thể cung cấp tên ( một đoạn code phức tạp hơn). Một cách tùy chọn, bạn có thể sử dụng option gọi là wrap_class thay vì .nested_fields.
(Chú ý rằng javascript được tạo ra đằng sau một link dựa trên sự hiện diện của một lớp wrapper). (default.nested-fields).
Ví dụ:
Default
= link_to_remove_association('remove this', @form_obj,
  { wrapper_class: 'my-wrapper-class' })
Callbacks (Khi thêm vào và xóa bỏ các items)
cocoon:before-insert: gọi trước khi chèn thêm một nested item.
cocoon:after-insert: gọi sau khi chèn cocoon:before-remove: gọi trước khi remove cocoon:after-remove: gọi đến sau khi remove item.
Sự kiện được áp dụng trong Javascript:
Default
$('#container').on('cocoon:before-insert', function(e, insertedItem) {
    // ... do something
  });
Nếu trong view của bạn có một đoạn code để chọn một owner:
Default
#owner
  #owner_from_list
    = f.association :owner, collection: Person.all(order: 'name'), prompt: 'Choose an existing owner'
  = link_to_add_association 'add a new person as owner', f, :owner
Điều này sẽ cho phép bạn chọn một owner trong list các person.
Callback được gọi như sau:
JavaScript
$(document).ready(function() {
    $('#owner')
      .on('cocoon:before-insert', function() {
        $("#owner_from_list").hide();
        $("#owner a.add_fields").hide();
      })
      .on('cocoon:after-insert', function() {
        /* ... do something ... */
      })
      .on("cocoon:before-remove", function() {
        $("#owner_from_list").show();
        $("#owner a.add_fields").show();
      })
      .on("cocoon:after-remove", function() {
        /* e.g. recalculate order of child items */
      });
    // example showing manipulating the inserted/removed item
    $('#tasks')
      .on('cocoon:before-insert', function(e,task_to_be_added) {
        task_to_be_added.fadeIn('slow');
      })
      .on('cocoon:after-insert', function(e, added_task) {
        // e.g. set the background of inserted task
        added_task.css("background","red");
      })
      .on('cocoon:before-remove', function(e, task) {
        // allow some time for the animation to complete
        $(this).data('remove-timeout', 1000);
        task.fadeOut('slow');
      });
});
Kiểm soát các hành vi insert
Mặc định sẽ được insert vào phía sau của container hiện tại, tuy nhiên ta có thể định nghĩa hai thuộc tính cho phép chèn vào một vị trí khác với mặc định
Default
$(document).ready(function() {
    $("#owner a.add_fields").
      data("association-insertion-method", 'before').
      data("association-insertion-node", 'this');
});
Ta có thể thay thế vị trí before bằng afterappendprepend.
Ví dụ:
Default
$(document).ready(function() {
    $("#owner a.add_fields").
      data("association-insertion-method", 'append').
      data("association-insertion-traversal", 'closest').
      data("association-insertion-node", '#parent_table');
});
Partial
Nếu tên đường dẫn không rõ ràng, thì cocoon sẽ hiểu mặc định là __fields. Để overide default thì dùng :partial.
Để bắt Javascript đúng, ta nên bắt đầu với một container (ví dụ div) của class .nested-fields hoặc một class định nghĩa method link_to_remove_association
Mặc dù không có giới hạn về số lượng nested.
Lời kết:
Bài viết này tôi đã giới thiệu đến các bạn công cụ hỗ trợ cho form nested dynamic và các cách tùy biến.

Comments