diff --git a/README.md b/README.md index ee293bc5..465658c5 100644 --- a/README.md +++ b/README.md @@ -79,10 +79,15 @@ It is often desirable to move the nested fields into a partial to keep things or In this case it will look for a partial called "task_fields" and pass the form builder as an `f` variable to it. -## Specifying a Target for Nested Fields +## Options for the Wrapper By default, `link_to_add` appends fields immediately before the link when -clicked. This is not desirable when using a list or table, for example. In +clicked. The contents of `fields_for` are wrapped inside a `div` with +`class="fields"`. + +### Specifying a Target for Nested Fields + +This behaviour is not desirable when using a list or table, for example. In these situations, the "data-target" attribute can be used to specify where new fields should be inserted. @@ -98,14 +103,37 @@ fields should be inserted.

<%= f.link_to_add "Add a task", :tasks, :data => { :target => "#tasks" } %>

``` +### Specifying a custom Wrapper Selector + +By default, nested_form assumes that the wrapper has a `class="fields"` +attribute. In case this class conflicts with your existing css, you may +provide a custom wrapper css selector attribute along with the `:wrapper +=> false` option. + +```erb + + <%= f.fields_for :tasks, :wrapper => false do |task_form| %> + + + + + <% end %> +
<%= task_form.text_field :name %><%= task_form.link_to_remove "Remove this task" %>
+

<%= f.link_to_add "Add a task", :tasks, :data => { :target => "#tasks", :selector => ".task-wrapper" } %>

+``` + +### Data Attribute Syntax + Note that the `:data` option above only works in Rails 3.1+. For Rails 3.0 and below, the following syntax must be used. ```erb -

<%= f.link_to_add "Add a task", :tasks, "data-target" => "#tasks" %>

+

<%= f.link_to_add "Add a task", :tasks, "data-target" => "#tasks", "data-selector" => ".task-wrapper" %>

``` + + ## JavaScript events Sometimes you want to do some additional work after element was added or removed, but only diff --git a/spec/dummy/app/controllers/tasks_controller.rb b/spec/dummy/app/controllers/tasks_controller.rb new file mode 100644 index 00000000..5423a9d9 --- /dev/null +++ b/spec/dummy/app/controllers/tasks_controller.rb @@ -0,0 +1,5 @@ +class TasksController < ApplicationController + def new + @task = Task.new + end +end diff --git a/spec/dummy/app/views/tasks/new.html.erb b/spec/dummy/app/views/tasks/new.html.erb new file mode 100644 index 00000000..415e7c5c --- /dev/null +++ b/spec/dummy/app/views/tasks/new.html.erb @@ -0,0 +1,12 @@ +<%= nested_form_for @task do |f| -%> + <%= f.text_field :name %> + + <%= f.fields_for :milestones, :wrapper => false do |milestone_form| %> + + + + + <% end %> +
<%= milestone_form.text_field :name %><%= milestone_form.link_to_remove "Remove" %>
+

<%= f.link_to_add "Add new milestone", :milestones, "data-target" => "#tasks", "data-selector" => ".milestones-wrapper" %>

+<% end %> diff --git a/spec/dummy/config/routes.rb b/spec/dummy/config/routes.rb index 6f29827f..52c74b70 100644 --- a/spec/dummy/config/routes.rb +++ b/spec/dummy/config/routes.rb @@ -1,6 +1,7 @@ Dummy::Application.routes.draw do resources :companies, :only => %w(new create) resources :projects, :only => %w(new create) + resources :tasks, :only => %w(new create) get '/:controller/:action' # The priority is based upon order of creation: diff --git a/spec/form_spec.rb b/spec/form_spec.rb index 2701051b..aad3b5f2 100644 --- a/spec/form_spec.rb +++ b/spec/form_spec.rb @@ -21,6 +21,25 @@ def check_form fields.reject { |field| field.visible? }.count.should == 1 end + def check_form_with_custom_wrapper + page.should have_no_css('form .milestones-wrapper input[id$=name]') + click_link 'Add new milestone' + page.should have_css('form .milestones-wrapper[data-nested-wrapper] input[id$=name]', :count => 1) + find('form .milestones-wrapper[data-nested-wrapper] input[id$=name]').should be_visible + find('form .milestones-wrapper[data-nested-wrapper] input[id$=_destroy]').value.should == 'false' + + click_link 'Remove' + find('form .milestones-wrapper[data-nested-wrapper] input[id$=_destroy]').value.should == '1' + find('form .milestones-wrapper[data-nested-wrapper] input[id$=name]').should_not be_visible + + click_link 'Add new milestone' + click_link 'Add new milestone' + fields = all('form .milestones-wrapper[data-nested-wrapper]') + fields.select { |field| field.visible? }.count.should == 2 + fields.reject { |field| field.visible? }.count.should == 1 + end + + it 'should work with jQuery', :js => true do visit '/projects/new' check_form @@ -63,4 +82,19 @@ def check_form name.should match(/\Acompany\[project_attributes\]\[tasks_attributes\]\[\d+\]\[milestones_attributes\]\[\d+\]\[name\]\z/) end + context "form with custom wrapper" do + + it 'should work with jQuery', :js => true do + visit '/tasks/new' + check_form_with_custom_wrapper + end + + it 'should work with Prototype', :js => true do + visit '/tasks/new?type=prototype' + check_form_with_custom_wrapper + end + + end + + end diff --git a/vendor/assets/javascripts/jquery_nested_form.js b/vendor/assets/javascripts/jquery_nested_form.js index b9225b14..56bff81d 100644 --- a/vendor/assets/javascripts/jquery_nested_form.js +++ b/vendor/assets/javascripts/jquery_nested_form.js @@ -11,10 +11,11 @@ var assoc = $(link).data('association'); // Name of child var blueprint = $('#' + $(link).data('blueprint-id')); var content = blueprint.data('blueprint'); // Fields template + var wrapperSelector = $(link).data('selector') || ".fields"; // Make the context correct by replacing with the generated ID // of each of the parent objects - var context = ($(link).closest('.fields').closestChild('input, textarea, select').eq(0).attr('name') || '').replace(/\[[a-z_]+\]$/, ''); + var context = ($(link).closest(wrapperSelector).closestChild('input, textarea, select').eq(0).attr('name') || '').replace(/\[[a-z_]+\]$/, ''); // If the parent has no inputs we need to strip off the last pair var current = content.match(new RegExp('\\[([a-z_]+)\\]\\[new_' + assoc + '\\]'))[1]; @@ -60,10 +61,15 @@ }, insertFields: function(content, assoc, link) { var target = $(link).data('target'); + var contentElement = $(content); + + //Add data-nested-wrapper attribute in order to allow remove links to find the wrapper + contentElement.attr("data-nested-wrapper", true); + if (target) { - return $(content).appendTo($(target)); + return contentElement.appendTo($(target)); } else { - return $(content).insertBefore(link); + return contentElement.insertBefore(link); } }, removeFields: function(e) { @@ -73,7 +79,7 @@ var hiddenField = $link.prev('input[type=hidden]'); hiddenField.val('1'); - var field = $link.closest('.fields'); + var field = $link.closest('*[data-nested-wrapper]'); field.hide(); field diff --git a/vendor/assets/javascripts/prototype_nested_form.js b/vendor/assets/javascripts/prototype_nested_form.js index 26733a69..866a264f 100644 --- a/vendor/assets/javascripts/prototype_nested_form.js +++ b/vendor/assets/javascripts/prototype_nested_form.js @@ -5,10 +5,11 @@ document.observe('click', function(e, el) { var target = el.readAttribute('data-target'); var blueprint = $(el.readAttribute('data-blueprint-id')); var content = blueprint.readAttribute('data-blueprint'); // Fields template + var wrapperSelector = el.readAttribute('data-selector') || ".fields"; // Make the context correct by replacing with the generated ID // of each of the parent objects - var context = (el.getOffsetParent('.fields').firstDescendant().readAttribute('name') || '').replace(/\[[a-z_]+\]$/, ''); + var context = (el.getOffsetParent(wrapperSelector).firstDescendant().readAttribute('name') || '').replace(/\[[a-z_]+\]$/, ''); // If the parent has no inputs we need to strip off the last pair var current = content.match(new RegExp('\\[([a-z_]+)\\]\\[new_' + assoc + '\\]'))[1]; @@ -43,11 +44,19 @@ document.observe('click', function(e, el) { content = content.replace(regexp, new_id); var field; + var wrapper; + if (target) { field = $$(target)[0].insert(content); + wrapper = field.select(wrapperSelector).last(); } else { field = el.insert({ before: content }); + wrapper = field.previous(wrapperSelector); } + + //Add data-nested-wrapper attribute in order to allow remove links to find the wrapper + wrapper.writeAttribute("data-nested-wrapper", true); + field.fire('nested:fieldAdded', {field: field}); field.fire('nested:fieldAdded:' + assoc, {field: field}); return false; @@ -61,7 +70,11 @@ document.observe('click', function(e, el) { if(hidden_field) { hidden_field.value = '1'; } - var field = el.up('.fields').hide(); + + var field = $(el).ancestors().detect(function(ancestor){ + return ancestor.hasAttribute("data-nested-wrapper"); + }); + field.hide(); field.fire('nested:fieldRemoved', {field: field}); field.fire('nested:fieldRemoved:' + assoc, {field: field}); return false;