nested_form 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
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,10 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rspec/core/rake_task'
4
+
5
+ desc "Run RSpec"
6
+ RSpec::Core::RakeTask.new do |t|
7
+ t.verbose = false
8
+ end
9
+
10
+ task :default => :spec
@@ -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
@@ -0,0 +1,2 @@
1
+ require "nested_form/builder"
2
+ require "nested_form/view_helper"
@@ -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
+
@@ -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
+