Skip to content
This repository has been archived by the owner on Dec 12, 2021. It is now read-only.

Allow custom wrapper selector #278

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -98,14 +103,37 @@ fields should be inserted.
<p><%= f.link_to_add "Add a task", :tasks, :data => { :target => "#tasks" } %></p>
```

### 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
<table id="tasks">
<%= f.fields_for :tasks, :wrapper => false do |task_form| %>
<tr class="task-wrapper">
<td><%= task_form.text_field :name %></td>
<td><%= task_form.link_to_remove "Remove this task" %></td>
</tr>
<% end %>
</table>
<p><%= f.link_to_add "Add a task", :tasks, :data => { :target => "#tasks", :selector => ".task-wrapper" } %></p>
```

### 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
<p><%= f.link_to_add "Add a task", :tasks, "data-target" => "#tasks" %></p>
<p><%= f.link_to_add "Add a task", :tasks, "data-target" => "#tasks", "data-selector" => ".task-wrapper" %></p>
```




## JavaScript events

Sometimes you want to do some additional work after element was added or removed, but only
Expand Down
5 changes: 5 additions & 0 deletions spec/dummy/app/controllers/tasks_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class TasksController < ApplicationController
def new
@task = Task.new
end
end
12 changes: 12 additions & 0 deletions spec/dummy/app/views/tasks/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<%= nested_form_for @task do |f| -%>
<%= f.text_field :name %>
<table id="tasks">
<%= f.fields_for :milestones, :wrapper => false do |milestone_form| %>
<tr class="milestones-wrapper">
<td><%= milestone_form.text_field :name %></td>
<td><%= milestone_form.link_to_remove "Remove" %></td>
</tr>
<% end %>
</table>
<p><%= f.link_to_add "Add new milestone", :milestones, "data-target" => "#tasks", "data-selector" => ".milestones-wrapper" %></p>
<% end %>
1 change: 1 addition & 0 deletions spec/dummy/config/routes.rb
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
34 changes: 34 additions & 0 deletions spec/form_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
14 changes: 10 additions & 4 deletions vendor/assets/javascripts/jquery_nested_form.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <parents> 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];
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand Down
17 changes: 15 additions & 2 deletions vendor/assets/javascripts/prototype_nested_form.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <parents> 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];
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down