0

Dynamic nested form using 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:

class Project < ActiveRecord::Base
  has_many :tasks
  accepts_nested_attributes_for :tasks, reject_if: :all_blank, allow_destroy: true
end
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

 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 )

= 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:

.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

= 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

 .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

 = 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

.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 link closest, next, children, 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. before, after, append, 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:

= 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:

= 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):

= 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ụ:

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 đó:

= 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.

 = link_to_add_association('add something', @form_obj, :comments,
    force_non_association_create: true)

Mặc định sẽ là: :force_non_association_createfalse.

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 _<association-object-singular>_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ụ:

= 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:

$('#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:

#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:

$(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

$(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 after, append, prepend.

Ví dụ:

$(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à _<association-object_singular>_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.

bài viết được tham khảo từ Cocoon

Have fun!


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í