├── .gitignore ├── .rspec ├── .rvmrc ├── .travis.yml ├── CHANGELOG.rdoc ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── gemfiles ├── Gemfile.base ├── Gemfile.rails3_0 └── Gemfile.rails3_1 ├── lib ├── generators │ └── nested_form │ │ └── install_generator.rb ├── nested_form.rb └── nested_form │ ├── builder_mixin.rb │ ├── builders.rb │ ├── engine.rb │ └── view_helper.rb ├── nested_form.gemspec ├── spec ├── dummy │ ├── Rakefile │ ├── app │ │ ├── assets │ │ │ ├── javascripts │ │ │ │ ├── application.js │ │ │ │ ├── jquery.js │ │ │ │ ├── jquery_events_test.js │ │ │ │ ├── jquery_nested_form.js │ │ │ │ ├── projects.js │ │ │ │ ├── prototype.js │ │ │ │ ├── prototype_events_test.js │ │ │ │ └── prototype_nested_form.js │ │ │ └── stylesheets │ │ │ │ ├── application.css │ │ │ │ ├── companies.css │ │ │ │ └── projects.css │ │ ├── controllers │ │ │ ├── application_controller.rb │ │ │ ├── companies_controller.rb │ │ │ └── projects_controller.rb │ │ ├── helpers │ │ │ ├── application_helper.rb │ │ │ └── projects_helper.rb │ │ ├── mailers │ │ │ └── .gitkeep │ │ ├── models │ │ │ ├── .gitkeep │ │ │ ├── company.rb │ │ │ ├── milestone.rb │ │ │ ├── project.rb │ │ │ ├── project_task.rb │ │ │ └── task.rb │ │ └── views │ │ │ ├── companies │ │ │ └── new.html.erb │ │ │ ├── layouts │ │ │ └── application.html.erb │ │ │ └── projects │ │ │ ├── new.html.erb │ │ │ └── without_intermediate_inputs.html.erb │ ├── config.ru │ ├── config │ │ ├── application.rb │ │ ├── boot.rb │ │ ├── database.yml │ │ ├── environment.rb │ │ ├── environments │ │ │ ├── development.rb │ │ │ ├── production.rb │ │ │ └── test.rb │ │ ├── initializers │ │ │ ├── backtrace_silencers.rb │ │ │ ├── inflections.rb │ │ │ ├── mime_types.rb │ │ │ ├── secret_token.rb │ │ │ ├── session_store.rb │ │ │ └── wrap_parameters.rb │ │ ├── locales │ │ │ └── en.yml │ │ └── routes.rb │ ├── db │ │ ├── migrate │ │ │ ├── 20110710143903_initial_tables.rb │ │ │ ├── 20120819164528_add_association_with_class_name.rb │ │ │ └── 20130203095901_create_company.rb │ │ └── schema.rb │ ├── public │ │ ├── 404.html │ │ ├── 422.html │ │ ├── 500.html │ │ ├── favicon.ico │ │ └── javascripts │ ├── script │ │ └── rails │ ├── test │ │ ├── functional │ │ │ └── projects_controller_test.rb │ │ └── unit │ │ │ └── helpers │ │ │ └── projects_helper_test.rb │ └── tmp │ │ └── cache │ │ └── .gitkeep ├── events_spec.rb ├── form_spec.rb ├── nested_form │ ├── builder_spec.rb │ └── view_helper_spec.rb └── spec_helper.rb └── vendor └── assets └── javascripts ├── jquery_nested_form.js └── prototype_nested_form.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | **/*.swp 3 | *.gem 4 | Gemfile.lock 5 | .bundle 6 | log 7 | gemfiles/*.lock 8 | spec/dummy/tmp/ 9 | spec/dummy/db/*.sqlite3 10 | .rbx 11 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm use 1.9.2@nested_form --create 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.8.7 4 | - 1.9.3 5 | - jruby-18mode 6 | - jruby-19mode 7 | - rbx-18mode 8 | - rbx-19mode 9 | jdk: 10 | - openjdk6 11 | gemfile: 12 | - Gemfile 13 | - gemfiles/Gemfile.rails3_1 14 | - gemfiles/Gemfile.rails3_0 15 | before_script: "sh -c 'cd spec/dummy && rake db:migrate RAILS_ENV=test'" 16 | script: "xvfb-run rake" 17 | notifications: 18 | email: 19 | recipients: 20 | - just.lest@gmail.com 21 | -------------------------------------------------------------------------------- /CHANGELOG.rdoc: -------------------------------------------------------------------------------- 1 | 0.3.2 (April 5, 2013) 2 | 3 | * Render blueprints inside form (thanks taavo) 4 | 5 | * Make generated ids easily overridable in jQuery version (thanks ghostganz) 6 | 7 | * Support `nested_wrapper` option as an alternative to `wrapper` option in 8 | order to maintain compatibility with `simple_fields_for` helper (#219) 9 | 10 | * Allow DOM target to be specified when adding new nested form elements 11 | (thanks mhuggins) 12 | 13 | * Fix "has_one => has_many => has_many" name generation (thanks kevinrood and 14 | basvanwesting) 15 | 16 | 0.3.1 (November 5, 2012) 17 | 18 | * Raise ArgumentError when accepts_nested_attributes_for is missing 19 | 20 | * Fix wrapper element disabling feature (#202 and #214) 21 | 22 | 0.3.0 (October 23, 2012) 23 | 24 | * Fix issue when deeply nested and first level only has a select input (thanks eric88) 25 | 26 | * Fix context getting for input names with an underscore (thanks kevinrood) 27 | 28 | * Add option to toggle wrapper div behavior with wrapper option in fields_for helper 29 | 30 | * Ability to set object for blueprint using model_object option in link_to_add helper (thanks Baltazore) 31 | 32 | * Support for FormtasticBootstrapBuilder (thanks rvanlieshout) 33 | 34 | * Store blueprint html in data-blueprint attribute 35 | 36 | * Add the marked_for_destruction class on the div if the object is marked for destruction 37 | 38 | * Add parent association name to the blueprint div id 39 | 40 | 0.2.3 (August 23, 2012) 41 | 42 | * Fix selector for deeply nested forms (thanks groe) 43 | 44 | * Fix association detection in #link_to remove (thanks nashbridges) 45 | 46 | * Add nested:fieldRemoved:type event (thanks nashbridges) 47 | 48 | * Add events for Prototype (thanks nashbridges) 49 | 50 | * Element.up() is the proper Prototype counter part to jQuery's closest() (thanks clemens) 51 | 52 | 0.2.2 (July 9, 2012) 53 | 54 | * Make deeply-nested form working in jruby and rubinius 55 | 56 | * Revert the "context" selector changes from 0.2.1 in order to work with jQuery 1.7.2 57 | 58 | 0.2.1 (June 4, 2012) 59 | 60 | * Added Travis integration (thanks fxposter) 61 | 62 | * Make the "context" selector stricter, to work with deeply-nested forms. (thanks groe and nickhoffman) 63 | 64 | * Include vendor folder in the gem for Rails 3.1 asset support (thanks dmarkow) 65 | 66 | 67 | 0.2.0 (February 7, 2012) 68 | 69 | * Integration tests (thanks fxposter) - issue #58 70 | 71 | * Improved simple_form and 3.1 support (thanks fxposter) 72 | 73 | * nested:fieldAdded event includes the newly added field (thanks harrigan) 74 | 75 | * other minor bug fixes 76 | 77 | 78 | 0.1.1 (April 23, 2011) 79 | 80 | * Support HTML options and block in add/remove link - issue #31 81 | 82 | * Added new RegEx to generate new objects IDs correctly - issue #26 and issue #30 83 | 84 | 85 | 0.1.0 (March 26, 2011) 86 | 87 | * Prefix new records with "new_" so there's no possible conflict with existing records - issue #21 88 | 89 | * Add support for _fields partial if no block is passed in to fields_for 90 | 91 | * Use the $-jquery-function only inside the jQuery scope (thanks nhocki) 92 | 93 | * Triggers nested:fieldAdded and nested:fieldRemoved events (thanks pirelenito) 94 | 95 | * Fixed JavaScript bug for nested attributes in has_one association (thanks pirelenito) 96 | 97 | 98 | 0.0.0 (February 17, 2011) 99 | 100 | * Initial release 101 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Please read before contributing 2 | 3 | 1) If you have any questions about NestedForm, search the [Wiki](https://github.com/ryanb/nested_form/wiki) or [Stack Overflow](http://stackoverflow.com). Do not post questions here. 4 | 5 | 2) If you find a security bug, **DO NOT** submit an issue here. Please send an e-mail to [just.lest@gmail.com](mailto:just.lest@gmail.com) instead. 6 | 7 | 3) Do a small search on the issues tracker before submitting your issue to see if it was already reported / fixed. In case it was not, create your report including Rails and NestedForm versions. If you are getting exceptions, please include the full backtrace into a gist. 8 | 9 | That's it! The more information you give, the more easy it becomes for us to track it down and fix it. Ideal scenario would be adding the issue to NestedForm test suite or to a sample application. 10 | 11 | Thanks! 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | gemspec :path => '.' 2 | 3 | instance_eval File.read(File.expand_path('../gemfiles/Gemfile.base', __FILE__)) 4 | 5 | gem 'rails', '~> 3.2.0' 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unmaintained 2 | 3 | The Nested Form gem is **no longer maintained**. Feel free to fork this project. 4 | 5 | # Nested Form 6 | 7 | [Build Status](http://travis-ci.org/ryanb/nested_form) 8 | 9 | This is a Rails gem for conveniently manage multiple nested models in a single form. It does so in an unobtrusive way through jQuery or Prototype. 10 | 11 | This gem only works with Rails 3. See the [rails2 branch](https://github.com/ryanb/nested_form/tree/rails2) for a plugin to work in Rails 2. 12 | 13 | An example project showing how this works is available in the [complex-nested-forms/nested_form branch](https://github.com/ryanb/complex-form-examples/tree/nested_form). 14 | 15 | 16 | ## Setup 17 | 18 | Add it to your Gemfile then run `bundle` to install it. 19 | 20 | ```ruby 21 | gem "nested_form" 22 | ``` 23 | 24 | And then add it to the Asset Pipeline in the application.js file: 25 | 26 | ``` 27 | //= require jquery_nested_form 28 | ``` 29 | 30 | ### Non Asset Pipeline Setup 31 | 32 | If you do not use the asset pipeline, run this generator to create the JavaScript file. 33 | 34 | ``` 35 | rails g nested_form:install 36 | ``` 37 | 38 | You can then include the generated JavaScript in your layout. 39 | 40 | ```erb 41 | <%= javascript_include_tag :defaults, "nested_form" %> 42 | ``` 43 | 44 | ## Usage 45 | 46 | Imagine you have a `Project` model that `has_many :tasks`. To be able to use this gem, you'll need to add `accepts_nested_attributes_for :tasks` to your Project model. If you wish to allow the nested objects to be destroyed, then add the `:allow_destroy => true` option to that declaration. See the [accepts_nested_attributes_for documentation](http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html#method-i-accepts_nested_attributes_for) for details on all available options. 47 | 48 | This will create a `tasks_attributes=` method, so you may need to add it to the `attr_accessible` array (`attr_accessible :tasks_attributes`). 49 | 50 | Then use the `nested_form_for` helper method to enable the nesting. 51 | 52 | ```erb 53 | <%= nested_form_for @project do |f| %> 54 | ``` 55 | 56 | You will then be able to use `link_to_add` and `link_to_remove` helper methods on the form builder in combination with fields_for to dynamically add/remove nested records. 57 | 58 | ```erb 59 | <%= f.fields_for :tasks do |task_form| %> 60 | <%= task_form.text_field :name %> 61 | <%= task_form.link_to_remove "Remove this task" %> 62 | <% end %> 63 |

<%= f.link_to_add "Add a task", :tasks %>

64 | ``` 65 | 66 | In order to choose how to handle, after validation errors, fields that are 67 | marked for destruction, the `marked_for_destruction` class is added on the div 68 | if the object is marked for destruction. 69 | 70 | ## Strong Parameters 71 | For Rails 4 or people using the "strong_parameters" gem, here is an example: 72 | 73 | ```ruby 74 | params.require(:project).permit(:name, tasks_attributes: [:id, :name, :_destroy]) 75 | ``` 76 | 77 | The `:id` is to make sure you do not end up with a whole lot of tasks. 78 | 79 | The `:_destroy` must be there so that we can delete tasks. 80 | 81 | ## SimpleForm and Formtastic Support 82 | 83 | Use `simple_nested_form_for` or `semantic_nested_form_for` for SimpleForm and Formtastic support respectively. 84 | 85 | 86 | ## Partials 87 | 88 | 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. 89 | 90 | ```erb 91 | <%= f.fields_for :tasks %> 92 | ``` 93 | 94 | In this case it will look for a partial called "task_fields" and pass the form builder as an `f` variable to it. 95 | 96 | 97 | ## Specifying a Target for Nested Fields 98 | 99 | By default, `link_to_add` appends fields immediately before the link when 100 | clicked. This is not desirable when using a list or table, for example. In 101 | these situations, the "data-target" attribute can be used to specify where new 102 | fields should be inserted. 103 | 104 | ```erb 105 | 106 | <%= f.fields_for :tasks, :wrapper => false do |task_form| %> 107 | 108 | 109 | 110 | 111 | <% end %> 112 |
<%= task_form.text_field :name %><%= task_form.link_to_remove "Remove this task" %>
113 |

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

114 | ``` 115 | 116 | Note that the `:data` option above only works in Rails 3.1+. For Rails 3.0 and 117 | below, the following syntax must be used. 118 | 119 | ```erb 120 |

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

121 | ``` 122 | 123 | 124 | ## JavaScript events 125 | 126 | Sometimes you want to do some additional work after element was added or removed, but only 127 | after DOM was _really_ modified. In this case simply listening for click events on 128 | 'Add new'/'Remove' link won't reliably work, because your code and code that inserts/removes 129 | nested field will run concurrently. 130 | 131 | This problem can be solved, because after adding or removing the field a set of custom events 132 | is triggered on this field. Using form example from above, if you click on the "Add a task" link, 133 | `nested:fieldAdded` and `nested:fieldAdded:tasks` will be triggered, while 134 | `nested:fieldRemoved` and `nested:fieldRemoved:tasks` will be triggered if you click 135 | "Remove this task" then. 136 | 137 | These events bubble up the DOM tree, going through `form` element, until they reach the `document`. 138 | This allows you to listen for the event and trigger some action accordingly. Field element, upon 139 | which action was made, is passed along with the `event` object. In jQuery you can access it 140 | via `event.field`, in Prototype the same field will be in `event.memo.field`. 141 | 142 | For example, you have a date input in a nested field and you want to use jQuery datepicker 143 | for it. This is a bit tricky, because you have to activate datepicker after field was inserted. 144 | 145 | ### jQuery 146 | 147 | ```javascript 148 | $(document).on('nested:fieldAdded', function(event){ 149 | // this field was just inserted into your form 150 | var field = event.field; 151 | // it's a jQuery object already! Now you can find date input 152 | var dateField = field.find('.date'); 153 | // and activate datepicker on it 154 | dateField.datepicker(); 155 | }) 156 | ``` 157 | 158 | ### Prototype 159 | 160 | ```javascript 161 | document.observe('nested:fieldAdded', function(event){ 162 | var field = event.memo.field; 163 | // it's already extended by Prototype 164 | var dateField = field.down('.date'); 165 | dateField.datepicker(); 166 | }) 167 | ``` 168 | 169 | Second type of event (i.e. `nested:fieldAdded:tasks`) is useful then you have more than one type 170 | of nested fields on a form (i.e. tasks and milestones) and want to distinguish, which exactly 171 | was added/deleted. 172 | 173 | See also [how to limit max count of nested fields](https://github.com/ryanb/nested_form/wiki/How-to:-limit-max-count-of-nested-fields) 174 | 175 | ## Enhanced jQuery JavaScript template 176 | 177 | You can override default behavior of inserting new subforms into your form. For example: 178 | 179 | ```javascript 180 | window.nestedFormEvents.insertFields = function(content, assoc, link) { 181 | return $(link).closest('form').find(assoc + '_fields').append($(content)); 182 | } 183 | ``` 184 | 185 | ## Contributing 186 | 187 | If you have any issues with Nested Form not addressed above or in the [example project](https://github.com/ryanb/complex-form-examples/tree/nested_form), please add an [issue on GitHub](https://github.com/ryanb/nested_form/issues) or [fork the project](https://help.github.com/articles/fork-a-repo) and send a [pull request](https://help.github.com/articles/using-pull-requests). To run the specs: 188 | 189 | ``` 190 | bundle install 191 | bundle exec rake spec:install 192 | bundle exec rake db:migrate 193 | bundle exec rake spec:all 194 | ``` 195 | 196 | See available rake tasks using `bundle exec rake -T`. 197 | 198 | ## Special Thanks 199 | 200 | 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). 201 | 202 | 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. 203 | 204 | Andrea also wrote a great [blog post](http://blog.madebydna.com/all/code/2010/10/07/dynamic-nested-froms-with-the-nested-form-gem.html) on the internal workings of this gem. 205 | 206 | Thanks [Pavel Forkert](https://github.com/fxposter) for the SimpleForm and Formtastic support. 207 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | 4 | begin 5 | require 'rspec/core/rake_task' 6 | desc "Run RSpec" 7 | RSpec::Core::RakeTask.new do |t| 8 | t.verbose = false 9 | end 10 | rescue LoadError 11 | puts "You should run rake spec:install in order to install all corresponding gems!" 12 | end 13 | 14 | task :default => :spec 15 | 16 | namespace :db do 17 | desc 'Prepare sqlite database' 18 | task :migrate do 19 | system 'cd spec/dummy && rake db:migrate RAILS_ENV=test && rake db:migrate RAILS_ENV=development' 20 | end 21 | end 22 | 23 | namespace :spec do 24 | desc 'Install gems from additional gemfiles' 25 | task :install do 26 | system 'bundle install' 27 | ENV.delete('GEM_HOME') 28 | ENV['BUNDLE_GEMFILE'] = File.expand_path('../gemfiles/Gemfile.rails3_1', __FILE__) 29 | system 'bundle install' 30 | ENV['BUNDLE_GEMFILE'] = File.expand_path('../gemfiles/Gemfile.rails3_0', __FILE__) 31 | system 'bundle install' 32 | end 33 | 34 | desc 'Run tests with Rails 3.1.x' 35 | task :rails3_1 do 36 | ENV.delete('GEM_HOME') 37 | ENV['BUNDLE_GEMFILE'] = File.expand_path('../gemfiles/Gemfile.rails3_1', __FILE__) 38 | Rake::Task["spec"].execute 39 | end 40 | 41 | desc 'Run tests with Rails 3.0.x' 42 | task :rails3_0 do 43 | ENV.delete('GEM_HOME') 44 | ENV['BUNDLE_GEMFILE'] = File.expand_path('../gemfiles/Gemfile.rails3_0', __FILE__) 45 | Rake::Task["spec"].execute 46 | end 47 | 48 | task :all do 49 | Rake::Task["spec"].execute 50 | Rake::Task["spec:rails3_1"].execute 51 | Rake::Task["spec:rails3_0"].execute 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.base: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'activerecord-jdbcsqlite3-adapter', :platforms => :jruby 3 | gem "sqlite3", :platforms => :ruby 4 | gem "simple_form" 5 | gem "formtastic" 6 | gem "formtastic-bootstrap" 7 | gem "rake" 8 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails3_0: -------------------------------------------------------------------------------- 1 | gemspec :path => '../' 2 | instance_eval File.read(File.expand_path('../Gemfile.base', __FILE__)) 3 | # forcing 3.0.20 (rather than 3.0.0) fixes a bug where bundle hangs 4 | # trying to fetch gems from github 5 | gem "rails", "~> 3.0.20" 6 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails3_1: -------------------------------------------------------------------------------- 1 | gemspec :path => '../' 2 | instance_eval File.read(File.expand_path('../Gemfile.base', __FILE__)) 3 | gem "rails", "~> 3.1.0" 4 | -------------------------------------------------------------------------------- /lib/generators/nested_form/install_generator.rb: -------------------------------------------------------------------------------- 1 | module NestedForm 2 | module Generators 3 | class InstallGenerator < Rails::Generators::Base 4 | def self.source_root 5 | File.expand_path('../../../../vendor/assets/javascripts', __FILE__) 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 18 | -------------------------------------------------------------------------------- /lib/nested_form.rb: -------------------------------------------------------------------------------- 1 | require "nested_form/engine" 2 | -------------------------------------------------------------------------------- /lib/nested_form/builder_mixin.rb: -------------------------------------------------------------------------------- 1 | module NestedForm 2 | module BuilderMixin 3 | # Adds a link to insert a new associated records. The first argument is the name of the link, the second is the name of the association. 4 | # 5 | # f.link_to_add("Add Task", :tasks) 6 | # 7 | # You can pass HTML options in a hash at the end and a block for the content. 8 | # 9 | # <%= f.link_to_add(:tasks, :class => "add_task", :href => new_task_path) do %> 10 | # Add Task 11 | # <% end %> 12 | # 13 | # You can also pass model_object option with an object for use in 14 | # the blueprint, e.g.: 15 | # 16 | # <%= f.link_to_add(:tasks, :model_object => Task.new(:name => 'Task')) %> 17 | # 18 | # See the README for more details on where to call this method. 19 | def link_to_add(*args, &block) 20 | options = args.extract_options!.symbolize_keys 21 | association = args.pop 22 | 23 | unless object.respond_to?("#{association}_attributes=") 24 | raise ArgumentError, "Invalid association. Make sure that accepts_nested_attributes_for is used for #{association.inspect} association." 25 | end 26 | 27 | model_object = options.delete(:model_object) do 28 | reflection = object.class.reflect_on_association(association) 29 | reflection.klass.new 30 | end 31 | 32 | options[:class] = [options[:class], "add_nested_fields"].compact.join(" ") 33 | options["data-association"] = association 34 | options["data-blueprint-id"] = fields_blueprint_id = fields_blueprint_id_for(association) 35 | args << (options.delete(:href) || "javascript:void(0)") 36 | args << options 37 | 38 | @fields ||= {} 39 | @template.after_nested_form(fields_blueprint_id) do 40 | blueprint = {:id => fields_blueprint_id, :style => 'display: none'} 41 | block, options = @fields[fields_blueprint_id].values_at(:block, :options) 42 | options[:child_index] = "new_#{association}" 43 | blueprint[:"data-blueprint"] = fields_for(association, model_object, options, &block).to_str 44 | @template.content_tag(:div, nil, blueprint) 45 | end 46 | @template.link_to(*args, &block) 47 | end 48 | 49 | # Adds a link to remove the associated record. The first argment is the name of the link. 50 | # 51 | # f.link_to_remove("Remove Task") 52 | # 53 | # You can pass HTML options in a hash at the end and a block for the content. 54 | # 55 | # <%= f.link_to_remove(:class => "remove_task", :href => "#") do %> 56 | # Remove Task 57 | # <% end %> 58 | # 59 | # See the README for more details on where to call this method. 60 | def link_to_remove(*args, &block) 61 | options = args.extract_options!.symbolize_keys 62 | options[:class] = [options[:class], "remove_nested_fields"].compact.join(" ") 63 | 64 | # Extracting "milestones" from "...[milestones_attributes][...]" 65 | md = object_name.to_s.match /(\w+)_attributes\](?:\[[\w\d]+\])?$/ 66 | association = md && md[1] 67 | options["data-association"] = association 68 | 69 | args << (options.delete(:href) || "javascript:void(0)") 70 | args << options 71 | hidden_field(:_destroy) << @template.link_to(*args, &block) 72 | end 73 | 74 | def fields_for_with_nested_attributes(association_name, *args) 75 | # TODO Test this better 76 | block = args.pop || Proc.new { |fields| @template.render(:partial => "#{association_name.to_s.singularize}_fields", :locals => {:f => fields}) } 77 | 78 | options = args.dup.extract_options! 79 | 80 | # Rails 3.0.x 81 | if options.empty? && args[0].kind_of?(Array) 82 | options = args[0].dup.extract_options! 83 | end 84 | 85 | @fields ||= {} 86 | @fields[fields_blueprint_id_for(association_name)] = { :block => block, :options => options } 87 | super(association_name, *(args << block)) 88 | end 89 | 90 | def fields_for_nested_model(name, object, options, block) 91 | classes = 'fields' 92 | classes << ' marked_for_destruction' if object.respond_to?(:marked_for_destruction?) && object.marked_for_destruction? 93 | 94 | perform_wrap = options.fetch(:nested_wrapper, true) 95 | perform_wrap &&= options[:wrapper] != false # wrap even if nil 96 | 97 | if perform_wrap 98 | @template.content_tag(:div, super, :class => classes) 99 | else 100 | super 101 | end 102 | end 103 | 104 | private 105 | 106 | def fields_blueprint_id_for(association) 107 | assocs = object_name.to_s.scan(/(\w+)_attributes/).map(&:first) 108 | assocs << association 109 | assocs.join('_') + '_fields_blueprint' 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/nested_form/builders.rb: -------------------------------------------------------------------------------- 1 | require 'nested_form/builder_mixin' 2 | 3 | module NestedForm 4 | class Builder < ::ActionView::Helpers::FormBuilder 5 | include ::NestedForm::BuilderMixin 6 | end 7 | 8 | begin 9 | require 'simple_form' 10 | class SimpleBuilder < ::SimpleForm::FormBuilder 11 | include ::NestedForm::BuilderMixin 12 | end 13 | rescue LoadError 14 | end 15 | 16 | begin 17 | require 'formtastic' 18 | class FormtasticBuilder < (defined?(::Formtastic::FormBuilder) ? Formtastic::FormBuilder : ::Formtastic::SemanticFormBuilder) 19 | include ::NestedForm::BuilderMixin 20 | end 21 | rescue LoadError 22 | end 23 | 24 | begin 25 | require 'formtastic-bootstrap' 26 | class FormtasticBootstrapBuilder < ::FormtasticBootstrap::FormBuilder 27 | include ::NestedForm::BuilderMixin 28 | end 29 | rescue LoadError 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/nested_form/engine.rb: -------------------------------------------------------------------------------- 1 | require 'rails' 2 | 3 | module NestedForm 4 | class Engine < ::Rails::Engine 5 | initializer 'nested_form' do |app| 6 | ActiveSupport.on_load(:action_view) do 7 | require "nested_form/view_helper" 8 | class ActionView::Base 9 | include NestedForm::ViewHelper 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/nested_form/view_helper.rb: -------------------------------------------------------------------------------- 1 | require 'nested_form/builders' 2 | 3 | module NestedForm 4 | module ViewHelper 5 | def nested_form_for(*args, &block) 6 | options = args.extract_options!.reverse_merge(:builder => NestedForm::Builder) 7 | form_for(*(args << options)) do |f| 8 | capture(f, &block).to_s << after_nested_form_callbacks 9 | end 10 | end 11 | 12 | if defined?(NestedForm::SimpleBuilder) 13 | def simple_nested_form_for(*args, &block) 14 | options = args.extract_options!.reverse_merge(:builder => NestedForm::SimpleBuilder) 15 | simple_form_for(*(args << options)) do |f| 16 | capture(f, &block).to_s << after_nested_form_callbacks 17 | end 18 | end 19 | end 20 | 21 | if defined?(NestedForm::FormtasticBuilder) 22 | def semantic_nested_form_for(*args, &block) 23 | options = args.extract_options!.reverse_merge(:builder => NestedForm::FormtasticBuilder) 24 | semantic_form_for(*(args << options)) do |f| 25 | capture(f, &block).to_s << after_nested_form_callbacks 26 | end 27 | end 28 | end 29 | 30 | if defined?(NestedForm::FormtasticBootstrapBuilder) 31 | def semantic_bootstrap_nested_form_for(*args, &block) 32 | options = args.extract_options!.reverse_merge(:builder => NestedForm::FormtasticBootstrapBuilder) 33 | semantic_form_for(*(args << options)) do |f| 34 | capture(f, &block).to_s << after_nested_form_callbacks 35 | end 36 | end 37 | end 38 | 39 | def after_nested_form(association, &block) 40 | @associations ||= [] 41 | @after_nested_form_callbacks ||= [] 42 | unless @associations.include?(association) 43 | @associations << association 44 | @after_nested_form_callbacks << block 45 | end 46 | end 47 | 48 | private 49 | def after_nested_form_callbacks 50 | @after_nested_form_callbacks ||= [] 51 | fields = [] 52 | while callback = @after_nested_form_callbacks.shift 53 | fields << callback.call 54 | end 55 | fields.join(" ").html_safe 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /nested_form.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = "nested_form" 3 | s.version = "0.3.2" 4 | s.authors = ["Ryan Bates", "Andrea Singh"] 5 | s.email = "ryan@railscasts.com" 6 | s.homepage = "http://github.com/ryanb/nested_form" 7 | s.summary = "Gem to conveniently handle multiple models in a single form." 8 | s.description = "Gem to conveniently handle multiple models in a single form with Rails 3 and jQuery or Prototype." 9 | 10 | s.files = Dir["{lib,spec,vendor}/**/*", "[A-Z]*"] - ["Gemfile.lock"] 11 | s.require_path = "lib" 12 | 13 | s.add_development_dependency "rake" 14 | s.add_development_dependency "bundler" 15 | s.add_development_dependency "rspec-rails", "~> 2.6" 16 | s.add_development_dependency "mocha" 17 | s.add_development_dependency "capybara", "~> 1.1" 18 | 19 | s.rubyforge_project = s.name 20 | s.required_rubygems_version = ">= 1.3.4" 21 | end 22 | -------------------------------------------------------------------------------- /spec/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # Add your own tasks in files placed in lib/tasks ending in .rake, 3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 4 | 5 | require File.expand_path('../config/application', __FILE__) 6 | 7 | Dummy::Application.load_tasks 8 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into including all the files listed below. 2 | // Add new JavaScript/Coffee code in separate files in this directory and they'll automatically 3 | // be included in the compiled file accessible from http://example.com/assets/application.js 4 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 5 | // the compiled file. 6 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/jquery_events_test.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | var log = function(text) { 3 | $('

', {text: text}).appendTo('#console'); 4 | }; 5 | 6 | ['Added', 'Removed'].forEach(function(action) { 7 | $(document).on('nested:field' + action, function(e) { 8 | log(action + ' some field') 9 | }); 10 | 11 | $(document).on('nested:field' + action + ':tasks', function(e) { 12 | log(action + ' task field') 13 | }); 14 | 15 | $(document).on('nested:field' + action + ':milestones', function(e) { 16 | log(action + ' milestone field') 17 | }); 18 | }); 19 | }); -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/jquery_nested_form.js: -------------------------------------------------------------------------------- 1 | ../../../../../vendor/assets/javascripts/jquery_nested_form.js -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/projects.js: -------------------------------------------------------------------------------- 1 | // Place all the behaviors and hooks related to the matching controller here. 2 | // All this logic will automatically be available in application.js. 3 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/prototype_events_test.js: -------------------------------------------------------------------------------- 1 | document.observe('dom:loaded', function() { 2 | var log = function(text) { 3 | var p = new Element('p').update(text); 4 | $('console').insert(p); 5 | }; 6 | 7 | ['Added', 'Removed'].forEach(function(action) { 8 | document.observe('nested:field' + action, function(e) { 9 | log(action + ' some field') 10 | }); 11 | 12 | document.observe('nested:field' + action + ':tasks', function(e) { 13 | log(action + ' task field') 14 | }); 15 | 16 | document.observe('nested:field' + action + ':milestones', function(e) { 17 | log(action + ' milestone field') 18 | }); 19 | }); 20 | }); -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/prototype_nested_form.js: -------------------------------------------------------------------------------- 1 | ../../../../../vendor/assets/javascripts/prototype_nested_form.js -------------------------------------------------------------------------------- /spec/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll automatically include all the stylesheets available in this directory 3 | * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at 4 | * the top of the compiled file, but it's generally better to create a new file per style scope. 5 | *= require_self 6 | *= require_tree . 7 | */ -------------------------------------------------------------------------------- /spec/dummy/app/assets/stylesheets/companies.css: -------------------------------------------------------------------------------- 1 | /* 2 | Place all the styles related to the matching controller here. 3 | They will automatically be included in application.css. 4 | */ 5 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/stylesheets/projects.css: -------------------------------------------------------------------------------- 1 | /* 2 | Place all the styles related to the matching controller here. 3 | They will automatically be included in application.css. 4 | */ 5 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/companies_controller.rb: -------------------------------------------------------------------------------- 1 | class CompaniesController < ApplicationController 2 | def new 3 | @company = Company.new 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/projects_controller.rb: -------------------------------------------------------------------------------- 1 | class ProjectsController < ApplicationController 2 | def new 3 | @project = Project.new 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy/app/helpers/projects_helper.rb: -------------------------------------------------------------------------------- 1 | module ProjectsHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy/app/mailers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/nested_form/78ad0469e681f159072e04c9c57d8722c9826295/spec/dummy/app/mailers/.gitkeep -------------------------------------------------------------------------------- /spec/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/nested_form/78ad0469e681f159072e04c9c57d8722c9826295/spec/dummy/app/models/.gitkeep -------------------------------------------------------------------------------- /spec/dummy/app/models/company.rb: -------------------------------------------------------------------------------- 1 | class Company < ActiveRecord::Base 2 | has_one :project 3 | accepts_nested_attributes_for :project 4 | 5 | after_initialize 'self.build_project' 6 | end 7 | -------------------------------------------------------------------------------- /spec/dummy/app/models/milestone.rb: -------------------------------------------------------------------------------- 1 | class Milestone < ActiveRecord::Base 2 | belongs_to :task 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/models/project.rb: -------------------------------------------------------------------------------- 1 | class Project < ActiveRecord::Base 2 | has_many :tasks 3 | has_many :assignments, :class_name => 'ProjectTask' 4 | accepts_nested_attributes_for :tasks 5 | accepts_nested_attributes_for :assignments 6 | 7 | has_many :not_nested_tasks, :class_name => 'Task' 8 | end 9 | -------------------------------------------------------------------------------- /spec/dummy/app/models/project_task.rb: -------------------------------------------------------------------------------- 1 | class ProjectTask < ActiveRecord::Base 2 | belongs_to :project 3 | end -------------------------------------------------------------------------------- /spec/dummy/app/models/task.rb: -------------------------------------------------------------------------------- 1 | class Task < ActiveRecord::Base 2 | belongs_to :project 3 | has_many :milestones 4 | accepts_nested_attributes_for :milestones 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/app/views/companies/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= nested_form_for @company do |f| -%> 2 | <%= f.text_field :name %> 3 | <%= f.fields_for :project do |pf| -%> 4 | <%= pf.text_field :name %> 5 | <%= pf.fields_for :tasks do |tf| -%> 6 | <%= tf.text_field :name %> 7 | <%= tf.fields_for :milestones do |mf| %> 8 | <%= mf.text_field :name %> 9 | <%= mf.link_to_remove 'Remove milestone' %> 10 | <% end %> 11 | <%= tf.link_to_add 'Add new milestone', :milestones %> 12 | <%= tf.link_to_remove 'Remove' %> 13 | <% end -%> 14 | <%= pf.link_to_add 'Add new task', :tasks %> 15 | <% end -%> 16 | <% end -%> 17 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | 6 | <% if params[:type] == 'prototype' %> 7 | <%= javascript_include_tag 'prototype', 'prototype_nested_form', 'prototype_events_test' %> 8 | <% else %> 9 | <%= javascript_include_tag 'jquery', 'jquery_nested_form', 'jquery_events_test' %> 10 | <% end %> 11 | 12 | 13 | 14 | <%= yield %> 15 | 16 |

17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /spec/dummy/app/views/projects/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= nested_form_for @project do |f| -%> 2 | <%= f.text_field :name %> 3 | <%= f.fields_for :tasks do |tf| -%> 4 | <%= tf.text_field :name %> 5 | <%= tf.fields_for :milestones do |mf| %> 6 | <%= mf.text_field :name %> 7 | <%= mf.link_to_remove 'Remove milestone' %> 8 | <% end %> 9 | <%= tf.link_to_add 'Add new milestone', :milestones %> 10 | <%= tf.link_to_remove 'Remove' %> 11 | <% end -%> 12 | <%= f.link_to_add 'Add new task', :tasks %> 13 | <% end -%> 14 | -------------------------------------------------------------------------------- /spec/dummy/app/views/projects/without_intermediate_inputs.html.erb: -------------------------------------------------------------------------------- 1 | <%= nested_form_for Project.new do |f| -%> 2 | <%= f.fields_for :tasks do |tf| -%> 3 | <%= tf.fields_for :milestones do |mf| %> 4 | <%= mf.text_field :name %> 5 | <%= mf.link_to_remove 'Remove milestone' %> 6 | <% end %> 7 | <%= tf.link_to_add 'Add new milestone', :milestones %> 8 | <%= tf.link_to_remove 'Remove' %> 9 | <% end -%> 10 | <%= f.link_to_add 'Add new task', :tasks %> 11 | <% end -%> 12 | -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Dummy::Application 5 | -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | Bundler.require 6 | require "nested_form" 7 | 8 | module Dummy 9 | class Application < Rails::Application 10 | # Settings in config/environments/* take precedence over those specified here. 11 | # Application configuration should go into files in config/initializers 12 | # -- all .rb files in that directory are automatically loaded. 13 | 14 | # Custom directories with classes and modules you want to be autoloadable. 15 | # config.autoload_paths += %W(#{config.root}/extras) 16 | 17 | # Only load the plugins named here, in the order given (default is alphabetical). 18 | # :all can be used as a placeholder for all plugins not explicitly named. 19 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 20 | 21 | # Activate observers that should always be running. 22 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 23 | 24 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 25 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 26 | # config.time_zone = 'Central Time (US & Canada)' 27 | 28 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 29 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 30 | # config.i18n.default_locale = :de 31 | 32 | # Configure the default encoding used in templates for Ruby 1.9. 33 | config.encoding = "utf-8" 34 | 35 | # Configure sensitive parameters which will be filtered from the log file. 36 | config.filter_parameters += [:password] 37 | 38 | # Enable the asset pipeline 39 | config.assets.enabled = true if config.respond_to?(:assets) 40 | end 41 | end 42 | 43 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | gemfile = ENV['BUNDLE_GEMFILE'] || File.expand_path('../../../../Gemfile', __FILE__) 4 | if File.exist?(gemfile) 5 | ENV['BUNDLE_GEMFILE'] = gemfile 6 | require 'bundler' 7 | Bundler.setup 8 | end 9 | 10 | $:.unshift File.expand_path('../../../../lib', __FILE__) 11 | -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | development: 7 | adapter: sqlite3 8 | database: db/development.sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | # Warning: The database defined as "test" will be erased and 13 | # re-generated from your development database when you run "rake". 14 | # Do not set this db to the same as development or production. 15 | test: 16 | adapter: sqlite3 17 | database: db/test.sqlite3 18 | pool: 5 19 | timeout: 5000 20 | 21 | production: 22 | adapter: sqlite3 23 | database: db/production.sqlite3 24 | pool: 5 25 | timeout: 5000 26 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | Dummy::Application.initialize! 6 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger 20 | config.active_support.deprecation = :log 21 | 22 | # Only use best-standards-support built into browsers 23 | config.action_dispatch.best_standards_support = :builtin 24 | 25 | # Do not compress assets 26 | config.assets.compress = false if config.respond_to?(:assets) 27 | end 28 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # Code is not reloaded between requests 5 | config.cache_classes = true 6 | 7 | # Full error reports are disabled and caching is turned on 8 | config.consider_all_requests_local = false 9 | config.action_controller.perform_caching = true 10 | 11 | # Disable Rails's static asset server (Apache or nginx will already do this) 12 | config.serve_static_assets = false 13 | 14 | # Compress JavaScripts and CSS 15 | config.assets.compress = true 16 | 17 | # Specify the default JavaScript compressor 18 | config.assets.js_compressor = :uglifier 19 | 20 | # Specifies the header that your server uses for sending files 21 | # (comment out if your front-end server doesn't support this) 22 | config.action_dispatch.x_sendfile_header = "X-Sendfile" # Use 'X-Accel-Redirect' for nginx 23 | 24 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 25 | # config.force_ssl = true 26 | 27 | # See everything in the log (default is :info) 28 | # config.log_level = :debug 29 | 30 | # Use a different logger for distributed setups 31 | # config.logger = SyslogLogger.new 32 | 33 | # Use a different cache store in production 34 | # config.cache_store = :mem_cache_store 35 | 36 | # Enable serving of images, stylesheets, and JavaScripts from an asset server 37 | # config.action_controller.asset_host = "http://assets.example.com" 38 | 39 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) 40 | # config.assets.precompile += %w( search.js ) 41 | 42 | # Disable delivery errors, bad email addresses will be ignored 43 | # config.action_mailer.raise_delivery_errors = false 44 | 45 | # Enable threaded mode 46 | # config.threadsafe! 47 | 48 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 49 | # the I18n.default_locale when a translation can not be found) 50 | config.i18n.fallbacks = true 51 | 52 | # Send deprecation notices to registered listeners 53 | config.active_support.deprecation = :notify 54 | end 55 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Configure static asset server for tests with Cache-Control for performance 11 | config.serve_static_assets = true 12 | config.static_cache_control = "public, max-age=3600" 13 | 14 | # Log error messages when you accidentally call methods on nil 15 | config.whiny_nils = true 16 | 17 | # Show full error reports and disable caching 18 | config.consider_all_requests_local = true 19 | config.action_controller.perform_caching = false 20 | 21 | # Raise exceptions instead of rendering exception templates 22 | config.action_dispatch.show_exceptions = false 23 | 24 | # Disable request forgery protection in test environment 25 | config.action_controller.allow_forgery_protection = false 26 | 27 | # Tell Action Mailer not to deliver emails to the real world. 28 | # The :test delivery method accumulates sent emails in the 29 | # ActionMailer::Base.deliveries array. 30 | config.action_mailer.delivery_method = :test 31 | 32 | # Use SQL instead of Active Record's schema dumper when creating the test database. 33 | # This is necessary if your schema can't be completely dumped by the schema dumper, 34 | # like if you have constraints or database-specific column types 35 | # config.active_record.schema_format = :sql 36 | 37 | # Print deprecation notices to the stderr 38 | config.active_support.deprecation = :stderr 39 | end 40 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | Dummy::Application.config.secret_token = '34d776d7b34c759d74790e5cc10edfaf610a403281e7cda53f0c25e750dc4b774d1a1a59d0ca3698e11ea260c57fcaa6c98b9aee9e62a95ffd9f7f91f21db18d' 8 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Dummy::Application.config.session_store :cookie_store, :key => '_dummy_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rails generate session_migration") 8 | # Dummy::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActionController::Base.wrap_parameters :format => [:json] if ActionController::Base.respond_to?(:wrap_parameters) 8 | 9 | # Disable root element in JSON by default. 10 | if defined?(ActiveRecord) 11 | ActiveRecord::Base.include_root_in_json = false 12 | end 13 | -------------------------------------------------------------------------------- /spec/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.routes.draw do 2 | resources :companies, :only => %w(new create) 3 | resources :projects, :only => %w(new create) 4 | get '/:controller/:action' 5 | 6 | # The priority is based upon order of creation: 7 | # first created -> highest priority. 8 | 9 | # Sample of regular route: 10 | # match 'products/:id' => 'catalog#view' 11 | # Keep in mind you can assign values other than :controller and :action 12 | 13 | # Sample of named route: 14 | # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase 15 | # This route can be invoked with purchase_url(:id => product.id) 16 | 17 | # Sample resource route (maps HTTP verbs to controller actions automatically): 18 | # resources :products 19 | 20 | # Sample resource route with options: 21 | # resources :products do 22 | # member do 23 | # get 'short' 24 | # post 'toggle' 25 | # end 26 | # 27 | # collection do 28 | # get 'sold' 29 | # end 30 | # end 31 | 32 | # Sample resource route with sub-resources: 33 | # resources :products do 34 | # resources :comments, :sales 35 | # resource :seller 36 | # end 37 | 38 | # Sample resource route with more complex sub-resources 39 | # resources :products do 40 | # resources :comments 41 | # resources :sales do 42 | # get 'recent', :on => :collection 43 | # end 44 | # end 45 | 46 | # Sample resource route within a namespace: 47 | # namespace :admin do 48 | # # Directs /admin/products/* to Admin::ProductsController 49 | # # (app/controllers/admin/products_controller.rb) 50 | # resources :products 51 | # end 52 | 53 | # You can have the root of your site routed with "root" 54 | # just remember to delete public/index.html. 55 | # root :to => 'welcome#index' 56 | 57 | # See how all your routes lay out with "rake routes" 58 | 59 | # This is a legacy wild controller route that's not recommended for RESTful applications. 60 | # Note: This route will make all actions in every controller accessible via GET requests. 61 | # match ':controller(/:action(/:id(.:format)))' 62 | end 63 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20110710143903_initial_tables.rb: -------------------------------------------------------------------------------- 1 | class InitialTables < ActiveRecord::Migration 2 | def self.up 3 | create_table :projects do |t| 4 | t.string :name 5 | end 6 | 7 | create_table :tasks do |t| 8 | t.integer :project_id 9 | t.string :name 10 | end 11 | 12 | create_table :milestones do |t| 13 | t.integer :task_id 14 | t.string :name 15 | end 16 | end 17 | 18 | def self.down 19 | drop_table :projects 20 | drop_table :tasks 21 | drop_table :milestones 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20120819164528_add_association_with_class_name.rb: -------------------------------------------------------------------------------- 1 | class AddAssociationWithClassName < ActiveRecord::Migration 2 | def self.up 3 | create_table :project_tasks do |t| 4 | t.integer :project_id 5 | t.string :name 6 | end 7 | end 8 | 9 | def self.down 10 | drop_table :project_tasks 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20130203095901_create_company.rb: -------------------------------------------------------------------------------- 1 | class CreateCompany < ActiveRecord::Migration 2 | def self.up 3 | create_table :companies do |t| 4 | t.string :name 5 | end 6 | 7 | add_column :projects, :company_id, :integer 8 | end 9 | 10 | def self.down 11 | remove_column :projects, :company_id 12 | drop_table :companies 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended to check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(:version => 20130203095901) do 15 | 16 | create_table "companies", :force => true do |t| 17 | t.string "name" 18 | end 19 | 20 | create_table "milestones", :force => true do |t| 21 | t.integer "task_id" 22 | t.string "name" 23 | end 24 | 25 | create_table "project_tasks", :force => true do |t| 26 | t.integer "project_id" 27 | t.string "name" 28 | end 29 | 30 | create_table "projects", :force => true do |t| 31 | t.string "name" 32 | t.integer "company_id" 33 | end 34 | 35 | create_table "tasks", :force => true do |t| 36 | t.integer "project_id" 37 | t.string "name" 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /spec/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The page you were looking for doesn't exist.

23 |

You may have mistyped the address or the page may have moved.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The change you wanted was rejected.

23 |

Maybe you tried to change something you didn't have access to.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

We're sorry, but something went wrong.

23 |

We've been notified about this issue and we'll take a look at it shortly.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/nested_form/78ad0469e681f159072e04c9c57d8722c9826295/spec/dummy/public/favicon.ico -------------------------------------------------------------------------------- /spec/dummy/public/javascripts: -------------------------------------------------------------------------------- 1 | ../app/assets/javascripts -------------------------------------------------------------------------------- /spec/dummy/script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /spec/dummy/test/functional/projects_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ProjectsControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/test/unit/helpers/projects_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ProjectsHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy/tmp/cache/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanb/nested_form/78ad0469e681f159072e04c9c57d8722c9826295/spec/dummy/tmp/cache/.gitkeep -------------------------------------------------------------------------------- /spec/events_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Nested form', :js => true do 4 | include Capybara::DSL 5 | 6 | [:jquery, :prototype].each do |js_framework| 7 | 8 | url = case js_framework 9 | when :jquery then '/projects/new' 10 | when :prototype then '/projects/new?type=prototype' 11 | end 12 | 13 | context "with #{js_framework}" do 14 | context 'when field was added' do 15 | it 'emits general add event' do 16 | visit url 17 | click_link 'Add new task' 18 | 19 | page.should have_content 'Added some field' 20 | end 21 | 22 | it 'emits add event for current association' do 23 | visit url 24 | click_link 'Add new task' 25 | 26 | page.should have_content 'Added task field' 27 | page.should_not have_content 'Added milestone field' 28 | 29 | click_link 'Add new milestone' 30 | 31 | page.should have_content 'Added milestone field' 32 | end 33 | end 34 | 35 | context 'when field was removed' do 36 | it 'emits general remove event' do 37 | visit url 38 | click_link 'Add new task' 39 | click_link 'Remove' 40 | 41 | page.should have_content 'Removed some field' 42 | end 43 | 44 | it 'emits remove event for current association' do 45 | visit url 46 | 2.times { click_link 'Add new task' } 47 | click_link 'Remove' 48 | 49 | page.should have_content 'Removed task field' 50 | page.should_not have_content 'Removed milestone field' 51 | 52 | click_link 'Add new milestone' 53 | click_link 'Remove milestone' 54 | 55 | page.should have_content 'Removed milestone field' 56 | end 57 | end 58 | end 59 | end 60 | end -------------------------------------------------------------------------------- /spec/form_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'NestedForm' do 4 | include Capybara::DSL 5 | 6 | def check_form 7 | page.should have_no_css('form .fields input[id$=name]') 8 | click_link 'Add new task' 9 | page.should have_css('form .fields input[id$=name]', :count => 1) 10 | find('form .fields input[id$=name]').should be_visible 11 | find('form .fields input[id$=_destroy]').value.should == 'false' 12 | 13 | click_link 'Remove' 14 | find('form .fields input[id$=_destroy]').value.should == '1' 15 | find('form .fields input[id$=name]').should_not be_visible 16 | 17 | click_link 'Add new task' 18 | click_link 'Add new task' 19 | fields = all('form .fields') 20 | fields.select { |field| field.visible? }.count.should == 2 21 | fields.reject { |field| field.visible? }.count.should == 1 22 | end 23 | 24 | it 'should work with jQuery', :js => true do 25 | visit '/projects/new' 26 | check_form 27 | end 28 | 29 | it 'should work with Prototype', :js => true do 30 | visit '/projects/new?type=prototype' 31 | check_form 32 | end 33 | 34 | it 'works when there are no inputs for intermediate association', :js => true do 35 | visit '/projects/without_intermediate_inputs' 36 | click_link 'Add new task' 37 | click_link 'Add new milestone' 38 | click_link 'Add new milestone' 39 | inputs = all('.fields .fields input[id$=name]') 40 | inputs.first[:name].should_not eq(inputs.last[:name]) 41 | end 42 | 43 | it 'generates correct name for the nested input', :js => true do 44 | visit '/projects/new?type=jquery' 45 | click_link 'Add new task' 46 | click_link 'Add new milestone' 47 | name = find('.fields .fields input[id$=name]')[:name] 48 | name.should match(/\Aproject\[tasks_attributes\]\[\d+\]\[milestones_attributes\]\[\d+\]\[name\]\z/) 49 | end 50 | 51 | it 'generates correct name for the nested input (has_one => has_many)', :js => true do 52 | visit '/companies/new?type=jquery' 53 | click_link 'Add new task' 54 | name = find('.fields .fields input[id$=name]')[:name] 55 | name.should match(/\Acompany\[project_attributes\]\[tasks_attributes\]\[\d+\]\[name\]\z/) 56 | end 57 | 58 | it 'generates correct name for the nested input (has_one => has_many => has_many)', :js => true do 59 | visit '/companies/new?type=jquery' 60 | click_link 'Add new task' 61 | click_link 'Add new milestone' 62 | name = find('.fields .fields .fields input[id$=name]')[:name] 63 | name.should match(/\Acompany\[project_attributes\]\[tasks_attributes\]\[\d+\]\[milestones_attributes\]\[\d+\]\[name\]\z/) 64 | end 65 | 66 | end 67 | -------------------------------------------------------------------------------- /spec/nested_form/builder_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | [NestedForm::Builder, NestedForm::SimpleBuilder, defined?(NestedForm::FormtasticBuilder) ? NestedForm::FormtasticBuilder : nil].compact.each do |builder| 4 | describe builder do 5 | let(:project) do 6 | Project.new 7 | end 8 | 9 | let(:template) do 10 | template = ActionView::Base.new 11 | template.output_buffer = "" 12 | template 13 | end 14 | 15 | context "with no options" do 16 | subject do 17 | builder.new(:item, project, template, {}, proc {}) 18 | end 19 | 20 | describe '#link_to_add' do 21 | it "behaves similar to a Rails link_to" do 22 | subject.link_to_add("Add", :tasks).should == 'Add' 23 | subject.link_to_add("Add", :tasks, :class => "foo", :href => "url").should == 'Add' 24 | subject.link_to_add(:tasks) { "Add" }.should == 'Add' 25 | end 26 | 27 | it 'raises ArgumentError when missing association is provided' do 28 | expect { 29 | subject.link_to_add('Add', :bugs) 30 | }.to raise_error(ArgumentError) 31 | end 32 | 33 | it 'raises ArgumentError when accepts_nested_attributes_for is missing' do 34 | expect { 35 | subject.link_to_add('Add', :not_nested_tasks) 36 | }.to raise_error(ArgumentError) 37 | end 38 | end 39 | 40 | describe '#link_to_remove' do 41 | it "behaves similar to a Rails link_to" do 42 | subject.link_to_remove("Remove").should == 'Remove' 43 | subject.link_to_remove("Remove", :class => "foo", :href => "url").should == 'Remove' 44 | subject.link_to_remove { "Remove" }.should == 'Remove' 45 | end 46 | 47 | it 'has data-association attribute' do 48 | project.tasks.build 49 | subject.fields_for(:tasks, :builder => builder) do |tf| 50 | tf.link_to_remove 'Remove' 51 | end.should match 'Remove' 52 | end 53 | 54 | context 'when association is declared in a model by the class_name' do 55 | it 'properly detects association name' do 56 | project.assignments.build 57 | subject.fields_for(:assignments, :builder => builder) do |tf| 58 | tf.link_to_remove 'Remove' 59 | end.should match 'Remove' 60 | end 61 | end 62 | 63 | context 'when there is more than one nested level' do 64 | it 'properly detects association name' do 65 | task = project.tasks.build 66 | task.milestones.build 67 | subject.fields_for(:tasks, :builder => builder) do |tf| 68 | tf.fields_for(:milestones, :builder => builder) do |mf| 69 | mf.link_to_remove 'Remove' 70 | end 71 | end.should match 'Remove' 72 | end 73 | end 74 | 75 | context 'has_one association' do 76 | let(:company) { Company.new } 77 | subject { builder.new(:item, company, template, {}, proc {}) } 78 | 79 | it 'properly detects association name' do 80 | subject.fields_for(:project, :builder => builder) do |f| 81 | f.link_to_remove 'Remove' 82 | end.should match 'Remove' 83 | end 84 | end 85 | end 86 | 87 | describe '#fields_for' do 88 | it "wraps nested fields each in a div with class" do 89 | 2.times { project.tasks.build } 90 | 91 | fields = if subject.is_a?(NestedForm::SimpleBuilder) 92 | subject.simple_fields_for(:tasks) { "Task" } 93 | else 94 | subject.fields_for(:tasks) { "Task" } 95 | end 96 | 97 | fields.should == '
Task
Task
' 98 | end 99 | end 100 | 101 | it "wraps nested fields marked for destruction with an additional class" do 102 | task = project.tasks.build 103 | task.mark_for_destruction 104 | fields = subject.fields_for(:tasks) { 'Task' } 105 | fields.should eq('
Task
') 106 | end 107 | 108 | it "puts blueprint into data-blueprint attribute" do 109 | task = project.tasks.build 110 | task.mark_for_destruction 111 | subject.fields_for(:tasks) { 'Task' } 112 | subject.link_to_add('Add', :tasks) 113 | output = template.send(:after_nested_form_callbacks) 114 | expected = ERB::Util.html_escape '
Task
' 115 | output.should match(/div.+data-blueprint="#{expected}"/) 116 | end 117 | 118 | it "adds parent association name to the blueprint div id" do 119 | task = project.tasks.build 120 | task.milestones.build 121 | subject.fields_for(:tasks, :builder => builder) do |tf| 122 | tf.fields_for(:milestones, :builder => builder) { 'Milestone' } 123 | tf.link_to_add('Add', :milestones) 124 | end 125 | output = template.send(:after_nested_form_callbacks) 126 | output.should match(/div.+id="tasks_milestones_fields_blueprint"/) 127 | end 128 | 129 | it "doesn't render wrapper div" do 130 | task = project.tasks.build 131 | fields = subject.fields_for(:tasks, :wrapper => false) { 'Task' } 132 | 133 | fields.should eq('Task') 134 | 135 | subject.link_to_add 'Add', :tasks 136 | output = template.send(:after_nested_form_callbacks) 137 | 138 | output.should match(/div.+data-blueprint="Task"/) 139 | end 140 | 141 | it "doesn't render wrapper div when collection is passed" do 142 | task = project.tasks.build 143 | fields = subject.fields_for(:tasks, project.tasks, :wrapper => false) { 'Task' } 144 | 145 | fields.should eq('Task') 146 | 147 | subject.link_to_add 'Add', :tasks 148 | output = template.send(:after_nested_form_callbacks) 149 | 150 | output.should match(/div.+data-blueprint="Task"/) 151 | end 152 | 153 | it "doesn't render wrapper with nested_wrapper option" do 154 | task = project.tasks.build 155 | fields = subject.fields_for(:tasks, :nested_wrapper => false) { 'Task' } 156 | 157 | fields.should eq('Task') 158 | 159 | subject.link_to_add 'Add', :tasks 160 | output = template.send(:after_nested_form_callbacks) 161 | 162 | output.should match(/div.+data-blueprint="Task"/) 163 | end 164 | end 165 | 166 | context "with options" do 167 | subject { builder.new(:item, project, template, {}, proc {}) } 168 | 169 | context "when model_object given" do 170 | it "should use it instead of new generated" do 171 | subject.fields_for(:tasks) {|f| f.object.name } 172 | subject.link_to_add("Add", :tasks, :model_object => Task.new(:name => 'for check')) 173 | output = template.send(:after_nested_form_callbacks) 174 | expected = ERB::Util.html_escape '
for check
' 175 | output.should match(/div.+data-blueprint="#{expected}"/) 176 | end 177 | end 178 | end 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /spec/nested_form/view_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe NestedForm::ViewHelper do 4 | include RSpec::Rails::HelperExampleGroup 5 | 6 | before(:each) do 7 | _routes.draw do 8 | resources :projects 9 | end 10 | end 11 | 12 | it "should pass nested form builder to form_for along with other options" do 13 | pending 14 | mock.proxy(_view).form_for(:first, :as => :second, :other => :arg, :builder => NestedForm::Builder) do |form_html| 15 | form_html 16 | end 17 | _view.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 | _view.nested_form_for(Project.new) do |f| 22 | f.should be_instance_of(NestedForm::Builder) 23 | end 24 | end 25 | 26 | it "should pass instance of NestedForm::SimpleBuilder to simple_nested_form_for block" do 27 | _view.simple_nested_form_for(Project.new) do |f| 28 | f.should be_instance_of(NestedForm::SimpleBuilder) 29 | end 30 | end 31 | 32 | if defined?(NestedForm::FormtasticBuilder) 33 | it "should pass instance of NestedForm::FormtasticBuilder to semantic_nested_form_for block" do 34 | _view.semantic_nested_form_for(Project.new) do |f| 35 | f.should be_instance_of(NestedForm::FormtasticBuilder) 36 | end 37 | end 38 | end 39 | 40 | if defined?(NestedForm::FormtasticBootstrapBuilder) 41 | it "should pass instance of NestedForm::FormtasticBootstrapBuilder to semantic_bootstrap_nested_form_for block" do 42 | _view.semantic_bootstrap_nested_form_for(Project.new) do |f| 43 | f.should be_instance_of(NestedForm::FormtasticBootstrapBuilder) 44 | end 45 | end 46 | end 47 | 48 | it "should append content to end of nested form" do 49 | _view.after_nested_form(:tasks) { _view.concat("123") } 50 | _view.after_nested_form(:milestones) { _view.concat("456") } 51 | result = _view.nested_form_for(Project.new) {} 52 | result.should include("123456") 53 | end 54 | 55 | if Rails.version >= "3.1.0" 56 | it "should set multipart when there's a file field" do 57 | _view.nested_form_for(Project.new) do |f| 58 | f.fields_for(:tasks) do |t| 59 | t.file_field :file 60 | end 61 | f.link_to_add "Add", :tasks 62 | end.should include(" enctype=\"multipart/form-data\" ") 63 | end 64 | end 65 | end 66 | 67 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # Configure Rails Environment 2 | ENV["RAILS_ENV"] = "test" 3 | 4 | require File.expand_path("../dummy/config/environment.rb", __FILE__) 5 | require 'rspec/rails' 6 | require 'capybara/rspec' 7 | 8 | Capybara.javascript_driver = :selenium 9 | RSpec.configure do |config| 10 | config.mock_with :mocha 11 | end 12 | 13 | Rails.backtrace_cleaner.remove_silencers! 14 | 15 | # Load support files 16 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 17 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/jquery_nested_form.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | window.NestedFormEvents = function() { 3 | this.addFields = $.proxy(this.addFields, this); 4 | this.removeFields = $.proxy(this.removeFields, this); 5 | }; 6 | 7 | NestedFormEvents.prototype = { 8 | addFields: function(e) { 9 | // Setup 10 | var link = e.currentTarget; 11 | var assoc = $(link).data('association'); // Name of child 12 | var blueprint = $('#' + $(link).data('blueprint-id')); 13 | var content = blueprint.data('blueprint'); // Fields template 14 | 15 | // Make the context correct by replacing with the generated ID 16 | // of each of the parent objects 17 | var context = ($(link).closest('.fields').closestChild('input, textarea, select').eq(0).attr('name') || '').replace(/\[[a-z_]+\]$/, ''); 18 | 19 | // If the parent has no inputs we need to strip off the last pair 20 | var current = content.match(new RegExp('\\[([a-z_]+)\\]\\[new_' + assoc + '\\]')); 21 | if (current) { 22 | context = context.replace(new RegExp('\\[' + current[1] + '\\]\\[(new_)?\\d+\\]$'), ''); 23 | } 24 | 25 | // context will be something like this for a brand new form: 26 | // project[tasks_attributes][1255929127459][assignments_attributes][1255929128105] 27 | // or for an edit form: 28 | // project[tasks_attributes][0][assignments_attributes][1] 29 | if (context) { 30 | var parentNames = context.match(/[a-z_]+_attributes(?=\]\[(new_)?\d+\])/g) || []; 31 | var parentIds = context.match(/[0-9]+/g) || []; 32 | 33 | for(var i = 0; i < parentNames.length; i++) { 34 | if(parentIds[i]) { 35 | content = content.replace( 36 | new RegExp('(_' + parentNames[i] + ')_.+?_', 'g'), 37 | '$1_' + parentIds[i] + '_'); 38 | 39 | content = content.replace( 40 | new RegExp('(\\[' + parentNames[i] + '\\])\\[.+?\\]', 'g'), 41 | '$1[' + parentIds[i] + ']'); 42 | } 43 | } 44 | } 45 | 46 | // Make a unique ID for the new child 47 | var regexp = new RegExp('new_' + assoc, 'g'); 48 | var new_id = this.newId(); 49 | content = $.trim(content.replace(regexp, new_id)); 50 | 51 | var field = this.insertFields(content, assoc, link); 52 | // bubble up event upto document (through form) 53 | field 54 | .trigger({ type: 'nested:fieldAdded', field: field }) 55 | .trigger({ type: 'nested:fieldAdded:' + assoc, field: field }); 56 | return false; 57 | }, 58 | newId: function() { 59 | return new Date().getTime(); 60 | }, 61 | insertFields: function(content, assoc, link) { 62 | var target = $(link).data('target'); 63 | if (target) { 64 | return $(content).appendTo($(target)); 65 | } else { 66 | return $(content).insertBefore(link); 67 | } 68 | }, 69 | removeFields: function(e) { 70 | var $link = $(e.currentTarget), 71 | assoc = $link.data('association'); // Name of child to be removed 72 | 73 | var hiddenField = $link.prev('input[type=hidden]'); 74 | hiddenField.val('1'); 75 | 76 | var field = $link.closest('.fields'); 77 | field.hide(); 78 | 79 | field 80 | .trigger({ type: 'nested:fieldRemoved', field: field }) 81 | .trigger({ type: 'nested:fieldRemoved:' + assoc, field: field }); 82 | return false; 83 | } 84 | }; 85 | 86 | window.nestedFormEvents = new NestedFormEvents(); 87 | $(document) 88 | .delegate('form a.add_nested_fields', 'click', nestedFormEvents.addFields) 89 | .delegate('form a.remove_nested_fields', 'click', nestedFormEvents.removeFields); 90 | })(jQuery); 91 | 92 | // http://plugins.jquery.com/project/closestChild 93 | /* 94 | * Copyright 2011, Tobias Lindig 95 | * 96 | * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) 97 | * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. 98 | * 99 | */ 100 | (function($) { 101 | $.fn.closestChild = function(selector) { 102 | // breadth first search for the first matched node 103 | if (selector && selector != '') { 104 | var queue = []; 105 | queue.push(this); 106 | while(queue.length > 0) { 107 | var node = queue.shift(); 108 | var children = node.children(); 109 | for(var i = 0; i < children.length; ++i) { 110 | var child = $(children[i]); 111 | if (child.is(selector)) { 112 | return child; //well, we found one 113 | } 114 | queue.push(child); 115 | } 116 | } 117 | } 118 | return $();//nothing found 119 | }; 120 | })(jQuery); 121 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/prototype_nested_form.js: -------------------------------------------------------------------------------- 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 target = el.readAttribute('data-target'); 6 | var blueprint = $(el.readAttribute('data-blueprint-id')); 7 | var content = blueprint.readAttribute('data-blueprint'); // Fields template 8 | 9 | // Make the context correct by replacing with the generated ID 10 | // of each of the parent objects 11 | var context = (el.getOffsetParent('.fields').firstDescendant().readAttribute('name') || '').replace(/\[[a-z_]+\]$/, ''); 12 | 13 | // If the parent has no inputs we need to strip off the last pair 14 | var current = content.match(new RegExp('\\[([a-z_]+)\\]\\[new_' + assoc + '\\]')); 15 | if (current) { 16 | context = context.replace(new RegExp('\\[' + current[1] + '\\]\\[(new_)?\\d+\\]$'), ''); 17 | } 18 | 19 | // context will be something like this for a brand new form: 20 | // project[tasks_attributes][1255929127459][assignments_attributes][1255929128105] 21 | // or for an edit form: 22 | // project[tasks_attributes][0][assignments_attributes][1] 23 | if(context) { 24 | var parent_names = context.match(/[a-z_]+_attributes(?=\]\[(new_)?\d+\])/g) || []; 25 | var parent_ids = context.match(/[0-9]+/g) || []; 26 | 27 | for(i = 0; i < parent_names.length; i++) { 28 | if(parent_ids[i]) { 29 | content = content.replace( 30 | new RegExp('(_' + parent_names[i] + ')_.+?_', 'g'), 31 | '$1_' + parent_ids[i] + '_'); 32 | 33 | content = content.replace( 34 | new RegExp('(\\[' + parent_names[i] + '\\])\\[.+?\\]', 'g'), 35 | '$1[' + parent_ids[i] + ']'); 36 | } 37 | } 38 | } 39 | 40 | // Make a unique ID for the new child 41 | var regexp = new RegExp('new_' + assoc, 'g'); 42 | var new_id = new Date().getTime(); 43 | content = content.replace(regexp, new_id); 44 | 45 | var field; 46 | if (target) { 47 | field = $$(target)[0].insert(content); 48 | } else { 49 | field = el.insert({ before: content }); 50 | } 51 | field.fire('nested:fieldAdded', {field: field}); 52 | field.fire('nested:fieldAdded:' + assoc, {field: field}); 53 | return false; 54 | } 55 | }); 56 | 57 | document.observe('click', function(e, el) { 58 | if (el = e.findElement('form a.remove_nested_fields')) { 59 | var hidden_field = el.previous(0), 60 | assoc = el.readAttribute('data-association'); // Name of child to be removed 61 | if(hidden_field) { 62 | hidden_field.value = '1'; 63 | } 64 | var field = el.up('.fields').hide(); 65 | field.fire('nested:fieldRemoved', {field: field}); 66 | field.fire('nested:fieldRemoved:' + assoc, {field: field}); 67 | return false; 68 | } 69 | }); 70 | --------------------------------------------------------------------------------