nested_form 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.rdoc +57 -0
- data/Rakefile +10 -0
- data/lib/generators/nested_form/install_generator.rb +17 -0
- data/lib/generators/nested_form/templates/jquery_nested_form.js +46 -0
- data/lib/generators/nested_form/templates/prototype_nested_form.js +48 -0
- data/lib/nested_form/builder.rb +32 -0
- data/lib/nested_form/view_helper.rb +26 -0
- data/lib/nested_form.rb +2 -0
- data/spec/nested_form/builder_spec.rb +36 -0
- data/spec/nested_form/view_helper_spec.rb +33 -0
- data/spec/spec_helper.rb +55 -0
- metadata +101 -0
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Ryan Bates
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
= Nested Form
|
2
|
+
|
3
|
+
A Rails gem to conveniently manage multiple nested models in a single form. It does so in an unobtrusive way through jQuery.
|
4
|
+
|
5
|
+
To learn more about how this works under the hood: http://blog.madebydna.com/dynamic-nested-forms-in-rails-3-with-the-nest
|
6
|
+
|
7
|
+
|
8
|
+
== Install
|
9
|
+
|
10
|
+
Add it to your Gemfile
|
11
|
+
|
12
|
+
gem "nested_form"
|
13
|
+
|
14
|
+
Run
|
15
|
+
|
16
|
+
bundle install
|
17
|
+
|
18
|
+
Run the generator
|
19
|
+
|
20
|
+
rails generate nested_form:install
|
21
|
+
|
22
|
+
|
23
|
+
== Usage
|
24
|
+
|
25
|
+
Running the generator will add a file at public/javascripts/nested_form.js which should be included after the jQuery or Prototype framework.
|
26
|
+
|
27
|
+
<%= javascript_include_tag :defaults, "nested_form" %>
|
28
|
+
|
29
|
+
You can then generate a nested form using the nested_form_for helper method.
|
30
|
+
|
31
|
+
<%= nested_form_for @project do |f| %>
|
32
|
+
|
33
|
+
Use this form just like normal, including the +fields_for+ helper method for nesting models. The benefit of this plugin comes from the +link_to_add+ and +link_to_remove+ helper methods on the form builder.
|
34
|
+
|
35
|
+
<%= f.fields_for :tasks do |task_form| %>
|
36
|
+
<%= task_form.text_field :name %>
|
37
|
+
<%= task_form.link_to_remove "Remove this task" %>
|
38
|
+
<% end %>
|
39
|
+
<%= f.link_to_add "Add a task", :tasks %>
|
40
|
+
|
41
|
+
This generates links which dynamically add and remove fields.
|
42
|
+
|
43
|
+
|
44
|
+
== Partials
|
45
|
+
|
46
|
+
It is often desirable to move the nested fields into a partial to keep things organized. If you don't supply a block to fields_for it will look for a partial and use that.
|
47
|
+
|
48
|
+
<%= f.fields_for :tasks %>
|
49
|
+
|
50
|
+
In this case it will look for a partial called "task_fields" and pass the form builder as an f variable to it.
|
51
|
+
|
52
|
+
|
53
|
+
== Special Thanks
|
54
|
+
|
55
|
+
This gem was originally based on the solution by Tim Riley in his {complex-form-examples fork}[https://github.com/timriley/complex-form-examples/tree/unobtrusive-jquery-deep-fix2].
|
56
|
+
|
57
|
+
Thank you Andrew Manshin for the Rails 3 transition, {Andrea Singh}[https://github.com/madebydna] for converting to a gem and {Peter Giacomo Lombardo}[https://github.com/pglombardo] for Prototype support.
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module NestedForm
|
2
|
+
module Generators
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
4
|
+
def self.source_root
|
5
|
+
File.dirname(__FILE__) + "/templates"
|
6
|
+
end
|
7
|
+
|
8
|
+
def copy_jquery_file
|
9
|
+
if File.exists?('public/javascripts/prototype.js')
|
10
|
+
copy_file 'prototype_nested_form.js', 'public/javascripts/nested_form.js'
|
11
|
+
else
|
12
|
+
copy_file 'jquery_nested_form.js', 'public/javascripts/nested_form.js'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
$(function() {
|
2
|
+
$('form a.add_nested_fields').live('click', function() {
|
3
|
+
// Setup
|
4
|
+
var assoc = $(this).attr('data-association'); // Name of child
|
5
|
+
var content = $('#' + assoc + '_fields_blueprint').html(); // Fields template
|
6
|
+
|
7
|
+
// Make the context correct by replacing new_<parents> with the generated ID
|
8
|
+
// of each of the parent objects
|
9
|
+
var context = ($(this).closest('.fields').find('input:first').attr('name') || '').replace(new RegExp('\[[a-z]+\]$'), '');
|
10
|
+
|
11
|
+
// context will be something like this for a brand new form:
|
12
|
+
// project[tasks_attributes][1255929127459][assignments_attributes][1255929128105]
|
13
|
+
// or for an edit form:
|
14
|
+
// project[tasks_attributes][0][assignments_attributes][1]
|
15
|
+
if(context) {
|
16
|
+
var parent_names = context.match(/[a-z_]+_attributes/g) || [];
|
17
|
+
var parent_ids = context.match(/[0-9]+/g);
|
18
|
+
|
19
|
+
for(i = 0; i < parent_names.length; i++) {
|
20
|
+
if(parent_ids[i]) {
|
21
|
+
content = content.replace(
|
22
|
+
new RegExp('(\\[' + parent_names[i] + '\\])\\[.+?\\]', 'g'),
|
23
|
+
'$1[' + parent_ids[i] + ']'
|
24
|
+
)
|
25
|
+
}
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
// Make a unique ID for the new child
|
30
|
+
var regexp = new RegExp('new_' + assoc, 'g');
|
31
|
+
var new_id = new Date().getTime();
|
32
|
+
content = content.replace(regexp, new_id);
|
33
|
+
|
34
|
+
$(this).before(content);
|
35
|
+
return false;
|
36
|
+
});
|
37
|
+
|
38
|
+
$('form a.remove_nested_fields').live('click', function() {
|
39
|
+
var hidden_field = $(this).prev('input[type=hidden]')[0];
|
40
|
+
if(hidden_field) {
|
41
|
+
hidden_field.value = '1';
|
42
|
+
}
|
43
|
+
$(this).closest('.fields').hide();
|
44
|
+
return false;
|
45
|
+
});
|
46
|
+
});
|
@@ -0,0 +1,48 @@
|
|
1
|
+
document.observe('click', function(e, el) {
|
2
|
+
if (el = e.findElement('form a.add_nested_fields')) {
|
3
|
+
// Setup
|
4
|
+
var assoc = el.readAttribute('data-association'); // Name of child
|
5
|
+
var content = $(assoc + '_fields_blueprint').innerHTML; // Fields template
|
6
|
+
|
7
|
+
// Make the context correct by replacing new_<parents> with the generated ID
|
8
|
+
// of each of the parent objects
|
9
|
+
var context = (el.getOffsetParent('.fields').firstDescendant().readAttribute('name') || '').replace(new RegExp('\[[a-z]+\]$'), '');
|
10
|
+
|
11
|
+
// context will be something like this for a brand new form:
|
12
|
+
// project[tasks_attributes][1255929127459][assignments_attributes][1255929128105]
|
13
|
+
// or for an edit form:
|
14
|
+
// project[tasks_attributes][0][assignments_attributes][1]
|
15
|
+
if(context) {
|
16
|
+
var parent_names = context.match(/[a-z_]+_attributes/g) || [];
|
17
|
+
var parent_ids = context.match(/[0-9]+/g);
|
18
|
+
|
19
|
+
for(i = 0; i < parent_names.length; i++) {
|
20
|
+
if(parent_ids[i]) {
|
21
|
+
content = content.replace(
|
22
|
+
new RegExp('(\\[' + parent_names[i] + '\\])\\[.+?\\]', 'g'),
|
23
|
+
'$1[' + parent_ids[i] + ']'
|
24
|
+
)
|
25
|
+
}
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
// Make a unique ID for the new child
|
30
|
+
var regexp = new RegExp('new_' + assoc, 'g');
|
31
|
+
var new_id = new Date().getTime();
|
32
|
+
content = content.replace(regexp, new_id);
|
33
|
+
|
34
|
+
el.insert({ before: content });
|
35
|
+
return false;
|
36
|
+
}
|
37
|
+
});
|
38
|
+
|
39
|
+
document.observe('click', function(e, el) {
|
40
|
+
if (el = e.findElement('form a.remove_nested_fields')) {
|
41
|
+
var hidden_field = el.previous(0);
|
42
|
+
if(hidden_field) {
|
43
|
+
hidden_field.value = '1';
|
44
|
+
}
|
45
|
+
el.ancestors()[0].hide();
|
46
|
+
return false;
|
47
|
+
}
|
48
|
+
});
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module NestedForm
|
2
|
+
class Builder < ::ActionView::Helpers::FormBuilder
|
3
|
+
def link_to_add(name, association)
|
4
|
+
@fields ||= {}
|
5
|
+
@template.after_nested_form(association) do
|
6
|
+
model_object = object.class.reflect_on_association(association).klass.new
|
7
|
+
output = %Q[<div id="#{association}_fields_blueprint" style="display: none">].html_safe
|
8
|
+
output << fields_for(association, model_object, :child_index => "new_#{association}", &@fields[association])
|
9
|
+
output.safe_concat('</div>')
|
10
|
+
output
|
11
|
+
end
|
12
|
+
@template.link_to(name, "javascript:void(0)", :class => "add_nested_fields", "data-association" => association)
|
13
|
+
end
|
14
|
+
|
15
|
+
def link_to_remove(name)
|
16
|
+
hidden_field(:_destroy) + @template.link_to(name, "javascript:void(0)", :class => "remove_nested_fields")
|
17
|
+
end
|
18
|
+
|
19
|
+
def fields_for_with_nested_attributes(association, args, block)
|
20
|
+
@fields ||= {}
|
21
|
+
@fields[association] = block
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
def fields_for_nested_model(name, association, args, block)
|
26
|
+
output = '<div class="fields">'.html_safe
|
27
|
+
output << super
|
28
|
+
output.safe_concat('</div>')
|
29
|
+
output
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module NestedForm
|
2
|
+
module ViewHelper
|
3
|
+
def nested_form_for(*args, &block)
|
4
|
+
options = args.extract_options!.reverse_merge(:builder => NestedForm::Builder)
|
5
|
+
output = form_for(*(args << options), &block)
|
6
|
+
@after_nested_form_callbacks ||= []
|
7
|
+
fields = @after_nested_form_callbacks.map do |callback|
|
8
|
+
callback.call
|
9
|
+
end
|
10
|
+
output << fields.join(" ").html_safe
|
11
|
+
end
|
12
|
+
|
13
|
+
def after_nested_form(association, &block)
|
14
|
+
@associations ||= []
|
15
|
+
@after_nested_form_callbacks ||= []
|
16
|
+
unless @associations.include?(association)
|
17
|
+
@associations << association
|
18
|
+
@after_nested_form_callbacks << block
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class ActionView::Base
|
25
|
+
include NestedForm::ViewHelper
|
26
|
+
end
|
data/lib/nested_form.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe NestedForm::Builder do
|
4
|
+
describe "with no options" do
|
5
|
+
before(:each) do
|
6
|
+
@project = Project.new
|
7
|
+
@template = ActionView::Base.new
|
8
|
+
@template.output_buffer = ""
|
9
|
+
@builder = NestedForm::Builder.new(:item, @project, @template, {}, proc {})
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should have an add link" do
|
13
|
+
@builder.link_to_add("Add", :tasks).should == '<a href="javascript:void(0)" class="add_nested_fields" data-association="tasks">Add</a>'
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should have a remove link" do
|
17
|
+
@builder.link_to_remove("Remove").should == '<input id="item__destroy" name="item[_destroy]" type="hidden" value="false" /><a href="javascript:void(0)" class="remove_nested_fields">Remove</a>'
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should wrap nested fields each in a div with class" do
|
21
|
+
2.times { @project.tasks.build }
|
22
|
+
@builder.fields_for(:tasks) do
|
23
|
+
"Task"
|
24
|
+
end.should == '<div class="fields">Task</div><div class="fields">Task</div>'
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should add task fields to hidden div after form" do
|
28
|
+
pending
|
29
|
+
output = ""
|
30
|
+
mock(@template).after_nested_form(:tasks) { |arg, block| output << block.call }
|
31
|
+
@builder.fields_for(:tasks) { "Task" }
|
32
|
+
@builder.link_to_add("Add", :tasks)
|
33
|
+
output.should == '<div id="tasks_fields_blueprint" style="display: none"><div class="fields">Task</div></div>'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe NestedForm::ViewHelper do
|
4
|
+
before(:each) do
|
5
|
+
@template = ActionView::Base.new
|
6
|
+
@template.output_buffer = ""
|
7
|
+
@template.stubs(:url_for).returns("")
|
8
|
+
@template.stubs(:projects_path).returns("")
|
9
|
+
@template.stubs(:protect_against_forgery?).returns(false)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should pass nested form builder to form_for along with other options" do
|
13
|
+
pending
|
14
|
+
mock.proxy(@template).form_for(:first, :as => :second, :other => :arg, :builder => NestedForm::Builder) do |form_html|
|
15
|
+
form_html
|
16
|
+
end
|
17
|
+
@template.nested_form_for(:first, :as => :second, :other => :arg) {"form"}
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should pass instance of NestedForm::Builder to nested_form_for block" do
|
21
|
+
@template.nested_form_for(Project.new) do |f|
|
22
|
+
f.should be_instance_of(NestedForm::Builder)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should append content to end of nested form" do
|
27
|
+
@template.after_nested_form(:tasks) { @template.concat("123") }
|
28
|
+
@template.after_nested_form(:milestones) { @template.concat("456") }
|
29
|
+
@template.nested_form_for(Project.new) {}
|
30
|
+
@template.output_buffer.should include("123456")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "bundler/setup"
|
3
|
+
|
4
|
+
require "action_view"
|
5
|
+
require "active_record"
|
6
|
+
|
7
|
+
Bundler.require(:default)
|
8
|
+
|
9
|
+
# require 'active_model'
|
10
|
+
# require 'active_record'
|
11
|
+
# require 'action_controller'
|
12
|
+
# require 'action_view'
|
13
|
+
# require 'action_view/template'
|
14
|
+
# require "active_support/all"
|
15
|
+
|
16
|
+
# require 'nested_form/view_helper'
|
17
|
+
# require 'nested_form/builder'
|
18
|
+
|
19
|
+
RSpec.configure do |config|
|
20
|
+
config.mock_with :mocha
|
21
|
+
end
|
22
|
+
|
23
|
+
class TablelessModel < ActiveRecord::Base
|
24
|
+
def self.columns() @columns ||= []; end
|
25
|
+
|
26
|
+
def self.column(name, sql_type = nil, default = nil, null = true)
|
27
|
+
columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.quoted_table_name
|
31
|
+
name.pluralize.underscore
|
32
|
+
end
|
33
|
+
|
34
|
+
def quoted_id
|
35
|
+
"0"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class Project < TablelessModel
|
40
|
+
column :name, :string
|
41
|
+
has_many :tasks
|
42
|
+
accepts_nested_attributes_for :tasks
|
43
|
+
end
|
44
|
+
|
45
|
+
class Task < TablelessModel
|
46
|
+
column :project_id, :integer
|
47
|
+
column :name, :string
|
48
|
+
belongs_to :project
|
49
|
+
end
|
50
|
+
|
51
|
+
class Milestone < TablelessModel
|
52
|
+
column :task_id, :integer
|
53
|
+
column :name, :string
|
54
|
+
belongs_to :task
|
55
|
+
end
|
metadata
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: nested_form
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.0
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ryan Bates
|
9
|
+
- Andrea Singh
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
|
14
|
+
date: 2011-02-15 00:00:00 -08:00
|
15
|
+
default_executable:
|
16
|
+
dependencies:
|
17
|
+
- !ruby/object:Gem::Dependency
|
18
|
+
name: rspec
|
19
|
+
prerelease: false
|
20
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
21
|
+
none: false
|
22
|
+
requirements:
|
23
|
+
- - ~>
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: 2.1.0
|
26
|
+
type: :development
|
27
|
+
version_requirements: *id001
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: mocha
|
30
|
+
prerelease: false
|
31
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
32
|
+
none: false
|
33
|
+
requirements:
|
34
|
+
- - ">="
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: "0"
|
37
|
+
type: :development
|
38
|
+
version_requirements: *id002
|
39
|
+
- !ruby/object:Gem::Dependency
|
40
|
+
name: rails
|
41
|
+
prerelease: false
|
42
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 3.0.0
|
48
|
+
type: :development
|
49
|
+
version_requirements: *id003
|
50
|
+
description: Gem to conveniently handle multiple models in a single form with Rails 3 and jQuery or Prototype.
|
51
|
+
email: ryan@railscasts.com
|
52
|
+
executables: []
|
53
|
+
|
54
|
+
extensions: []
|
55
|
+
|
56
|
+
extra_rdoc_files: []
|
57
|
+
|
58
|
+
files:
|
59
|
+
- lib/generators/nested_form/install_generator.rb
|
60
|
+
- lib/generators/nested_form/templates/jquery_nested_form.js
|
61
|
+
- lib/generators/nested_form/templates/prototype_nested_form.js
|
62
|
+
- lib/nested_form/builder.rb
|
63
|
+
- lib/nested_form/view_helper.rb
|
64
|
+
- lib/nested_form.rb
|
65
|
+
- spec/nested_form/builder_spec.rb
|
66
|
+
- spec/nested_form/view_helper_spec.rb
|
67
|
+
- spec/spec_helper.rb
|
68
|
+
- Gemfile
|
69
|
+
- LICENSE
|
70
|
+
- Rakefile
|
71
|
+
- README.rdoc
|
72
|
+
has_rdoc: true
|
73
|
+
homepage: http://github.com/ryanb/nested_form
|
74
|
+
licenses: []
|
75
|
+
|
76
|
+
post_install_message:
|
77
|
+
rdoc_options: []
|
78
|
+
|
79
|
+
require_paths:
|
80
|
+
- lib
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: "0"
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: 1.3.4
|
93
|
+
requirements: []
|
94
|
+
|
95
|
+
rubyforge_project: nested_form
|
96
|
+
rubygems_version: 1.5.0
|
97
|
+
signing_key:
|
98
|
+
specification_version: 3
|
99
|
+
summary: Gem to conveniently handle multiple models in a single form.
|
100
|
+
test_files: []
|
101
|
+
|