From bfe72bd29f1b461c737cd71ece2cf60ce42bea35 Mon Sep 17 00:00:00 2001 From: John Bintz Date: Thu, 25 Oct 2012 14:49:55 -0400 Subject: [PATCH] add support for sorting nested forms --- README.markdown | 56 +++++++++++++++++++ app/assets/javascripts/cocoon.js | 4 +- app/assets/javascripts/cocoon/ordered.js | 29 ++++++++++ .../stylesheets/cocoon/active_admin.css.scss | 7 +++ lib/cocoon/formtastic/cocoon_input.rb | 7 ++- 5 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 app/assets/javascripts/cocoon/ordered.js diff --git a/README.markdown b/README.markdown index d6c93f8..6d1bff1 100644 --- a/README.markdown +++ b/README.markdown @@ -16,6 +16,8 @@ This project is not related to [Apache Cocoon](http://cocoon.apache.org/) This gem uses jQuery, it is most useful to use this gem in a rails3 project where you are already using jQuery. +Sortable form support requires jQuery UI. + Furthermore i would advice you to use either formtastic or simple_form. I have a sample project where I demonstrate the use of cocoon with formtastic. @@ -37,6 +39,13 @@ asset_pipeline //= require cocoon ```` +If you also want to be able to sort nested forms, ordering them on a particular field, add `cocoon/ordered`: + +``` ruby +//= require cocoon +//= require cocoon/ordered +``` + ### Rails 3.0.x If you are using Rails 3.0.x, you need to run the installation task (since rails 3.1 this is no longer needed): @@ -125,6 +134,53 @@ That is all there is to it! There is an example project on github implementing it called [cocoon_formtastic_demo](https://github.com/nathanvda/cocoon_formtastic_demo). +Or, you can use the Formtastic `cocoon` field type to wrap up much of the boilerplate of the wrapper and +add association button: + +``` haml += f.inputs do + = f.input :name + = f.input :description + %h3 Tasks + #tasks + = f.input :tasks, :as => :cocoon + = f.actions do + = f.action :submit +``` + +#### Sortable forms + +Say you have a set of nested models that are ordered arbitrarily: + +``` ruby +class Task < ActiveRecord::Base + belongs_to :project + + default_scope :order => 'order ASC' +end +``` + +You want users to be able to sort those +models via the UI. You can do this by including `cocoon/ordered` and specifying the sort field in the Formtastic +input call: + +``` haml += f.input :tasks, :as => :cocoon, :ordered_by => :order +``` + +Add the order field as a hidden field in the nested form: + +``` haml +.nested-fields + = f.inputs do + = f.input :description + = f.input :done, :as => :boolean + = f.input :order, :as => :hidden + = link_to_remove_association "remove task", f +``` + +The order field will now be filled in correctly when new models are added and when the models are sorted. + ### Using simple_form Inside our `projects/_form` partial we then write: diff --git a/app/assets/javascripts/cocoon.js b/app/assets/javascripts/cocoon.js index 2418e6f..e41d296 100644 --- a/app/assets/javascripts/cocoon.js +++ b/app/assets/javascripts/cocoon.js @@ -51,14 +51,14 @@ var contentNode = $(new_content); - insertionNode.trigger('cocoon:before-insert'); + insertionNode.trigger('cocoon:before-insert', contentNode); // allow any of the jquery dom manipulation methods (after, before, append, prepend, etc) // to be called on the node. allows the insertion node to be the parent of the inserted // code and doesn't force it to be a sibling like after/before does. default: 'before' insertionNode[insertionMethod](contentNode); - insertionNode.trigger('cocoon:after-insert'); + insertionNode.trigger('cocoon:after-insert', contentNode); }); $('.remove_fields.dynamic').live('click', function(e) { diff --git a/app/assets/javascripts/cocoon/ordered.js b/app/assets/javascripts/cocoon/ordered.js new file mode 100644 index 0000000..37ed84a --- /dev/null +++ b/app/assets/javascripts/cocoon/ordered.js @@ -0,0 +1,29 @@ +//= require jquery-ui +// +(function($) { + $(function() { + $('li[data-ordered_by]').each(function(index, element) { + var field = $(element).data('ordered_by'); + var fieldSearch = "[name*='[" + field + "]']" + + $(element).sortable({ + items: '.nested-fields', + stop: function(e, ui) { + $(element).find(fieldSearch).each(function(index, element) { + $(element).val(index); + }); + } + }); + + $(element).bind('cocoon:after-insert', function(e, node) { + var nextOrder = 0; + + $(element).find(fieldSearch).each(function() { + nextOrder = Math.max(nextOrder, Number($(this).val()) + 1); + }); + + $(node).find(fieldSearch).val(nextOrder) + }); + }); + }); +})(jQuery); diff --git a/app/assets/stylesheets/cocoon/active_admin.css.scss b/app/assets/stylesheets/cocoon/active_admin.css.scss index 1399ad1..3678246 100644 --- a/app/assets/stylesheets/cocoon/active_admin.css.scss +++ b/app/assets/stylesheets/cocoon/active_admin.css.scss @@ -18,5 +18,12 @@ li.cocoon { .links a { @extend .button; } + + &.ui-sortable { + fieldset { + border-top: solid #777 12px; + cursor: move; + } + } } diff --git a/lib/cocoon/formtastic/cocoon_input.rb b/lib/cocoon/formtastic/cocoon_input.rb index 209f157..649782e 100644 --- a/lib/cocoon/formtastic/cocoon_input.rb +++ b/lib/cocoon/formtastic/cocoon_input.rb @@ -18,7 +18,12 @@ class CocoonInput template.link_to_add_association template.t('.add'), builder, method end - template.content_tag(:li, output.join('').html_safe, :class => 'input cocoon') + data = { :class => 'input cocoon' } + if options[:ordered_by] + data['data-ordered_by'] = options[:ordered_by] + end + + template.content_tag(:li, output.join('').html_safe, data) end end