We've been notified about this issue and we'll take a look at it shortly.
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/nested_form.gemspec:
--------------------------------------------------------------------------------
1 | Gem::Specification.new do |s|
2 | s.name = "nested_form"
3 | s.version = "0.2.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 |
18 | s.rubyforge_project = s.name
19 | s.required_rubygems_version = ">= 1.3.4"
20 | end
21 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 => 20110710143903) do
15 |
16 | create_table "milestones", :force => true do |t|
17 | t.integer "task_id"
18 | t.string "name"
19 | end
20 |
21 | create_table "projects", :force => true do |t|
22 | t.string "name"
23 | end
24 |
25 | create_table "tasks", :force => true do |t|
26 | t.integer "project_id"
27 | t.string "name"
28 | end
29 |
30 | end
31 |
--------------------------------------------------------------------------------
/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 | task :migrate do
18 | system 'cd spec/dummy && rake db:migrate RAILS_ENV=test && rake db:migrate RAILS_ENV=development'
19 | end
20 | end
21 |
22 | namespace :spec do
23 | task :install do
24 | system 'bundle install'
25 | ENV['BUNDLE_GEMFILE'] = File.expand_path('../gemfiles/Gemfile.rails3_1', __FILE__)
26 | system 'bundle install'
27 | ENV['BUNDLE_GEMFILE'] = File.expand_path('../gemfiles/Gemfile.rails3_0', __FILE__)
28 | system 'bundle install'
29 | end
30 |
31 | task :rails3_1 do
32 | ENV['BUNDLE_GEMFILE'] = File.expand_path('../gemfiles/Gemfile.rails3_1', __FILE__)
33 | Rake::Task["spec"].execute
34 | end
35 |
36 | task :rails3_0 do
37 | ENV['BUNDLE_GEMFILE'] = File.expand_path('../gemfiles/Gemfile.rails3_0', __FILE__)
38 | Rake::Task["spec"].execute
39 | end
40 |
41 | task :all do
42 | Rake::Task["spec"].execute
43 | Rake::Task["spec:rails3_1"].execute
44 | Rake::Task["spec:rails3_0"].execute
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/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 'generates correct name for the nested input', :js => true do
35 | visit '/projects/new?type=jquery'
36 | click_link 'Add new task'
37 | click_link 'Add new milestone'
38 | name = find('.fields .fields input[id$=name]')[:name]
39 | name.should match(/\Aproject\[tasks_attributes\]\[new_\d+\]\[milestones_attributes\]\[new_\d+\]\[name\]\z/)
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/CHANGELOG.rdoc:
--------------------------------------------------------------------------------
1 | 0.2.2 (July 9, 2012)
2 |
3 | * Make deeply-nested form working in jruby and rubinius
4 |
5 | * Revert the "context" selector changes from 0.2.1 in order to work with jQuery 1.7.2
6 |
7 | 0.2.1 (June 4, 2012)
8 |
9 | * Added Travis integration (thanks fxposter)
10 |
11 | * Make the "context" selector stricter, to work with deeply-nested forms. (thanks groe and nickhoffman)
12 |
13 | * Include vendor folder in the gem for Rails 3.1 asset support (thanks dmarkow)
14 |
15 |
16 | 0.2.0 (February 7, 2012)
17 |
18 | * Integration tests (thanks fxposter) - issue #58
19 |
20 | * Improved simple_form and 3.1 support (thanks fxposter)
21 |
22 | * nested:fieldAdded event includes the newly added field (thanks harrigan)
23 |
24 | * other minor bug fixes
25 |
26 |
27 | 0.1.1 (April 23, 2011)
28 |
29 | * Support HTML options and block in add/remove link - issue #31
30 |
31 | * Added new RegEx to generate new objects IDs correctly - issue #26 and issue #30
32 |
33 |
34 | 0.1.0 (March 26, 2011)
35 |
36 | * Prefix new records with "new_" so there's no possible conflict with existing records - issue #21
37 |
38 | * Add support for _fields partial if no block is passed in to fields_for
39 |
40 | * Use the $-jquery-function only inside the jQuery scope (thanks nhocki)
41 |
42 | * Triggers nested:fieldAdded and nested:fieldRemoved events (thanks pirelenito)
43 |
44 | * Fixed JavaScript bug for nested attributes in has_one association (thanks pirelenito)
45 |
46 |
47 | 0.0.0 (February 17, 2011)
48 |
49 | * Initial release
50 |
--------------------------------------------------------------------------------
/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), &block) << after_nested_form_callbacks
8 | end
9 |
10 | if defined?(NestedForm::SimpleBuilder)
11 | def simple_nested_form_for(*args, &block)
12 | options = args.extract_options!.reverse_merge(:builder => NestedForm::SimpleBuilder)
13 | simple_form_for(*(args << options), &block) << after_nested_form_callbacks
14 | end
15 | end
16 |
17 | if defined?(NestedForm::FormtasticBuilder)
18 | def semantic_nested_form_for(*args, &block)
19 | options = args.extract_options!.reverse_merge(:builder => NestedForm::FormtasticBuilder)
20 | semantic_form_for(*(args << options), &block) << after_nested_form_callbacks
21 | end
22 | end
23 |
24 | def after_nested_form(association, &block)
25 | @associations ||= []
26 | @after_nested_form_callbacks ||= []
27 | unless @associations.include?(association)
28 | @associations << association
29 | @after_nested_form_callbacks << block
30 | end
31 | end
32 |
33 | private
34 | def after_nested_form_callbacks
35 | @after_nested_form_callbacks ||= []
36 | fields = []
37 | while callback = @after_nested_form_callbacks.shift
38 | fields << callback.call
39 | end
40 | fields.join(" ").html_safe
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/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 | it "should append content to end of nested form" do
41 | _view.after_nested_form(:tasks) { _view.concat("123") }
42 | _view.after_nested_form(:milestones) { _view.concat("456") }
43 | _view.nested_form_for(Project.new) {}
44 | _view.output_buffer.should include("123456")
45 | end
46 | end
47 |
48 |
--------------------------------------------------------------------------------
/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/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/routes.rb:
--------------------------------------------------------------------------------
1 | Dummy::Application.routes.draw do
2 | resources :projects
3 | # The priority is based upon order of creation:
4 | # first created -> highest priority.
5 |
6 | # Sample of regular route:
7 | # match 'products/:id' => 'catalog#view'
8 | # Keep in mind you can assign values other than :controller and :action
9 |
10 | # Sample of named route:
11 | # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase
12 | # This route can be invoked with purchase_url(:id => product.id)
13 |
14 | # Sample resource route (maps HTTP verbs to controller actions automatically):
15 | # resources :products
16 |
17 | # Sample resource route with options:
18 | # resources :products do
19 | # member do
20 | # get 'short'
21 | # post 'toggle'
22 | # end
23 | #
24 | # collection do
25 | # get 'sold'
26 | # end
27 | # end
28 |
29 | # Sample resource route with sub-resources:
30 | # resources :products do
31 | # resources :comments, :sales
32 | # resource :seller
33 | # end
34 |
35 | # Sample resource route with more complex sub-resources
36 | # resources :products do
37 | # resources :comments
38 | # resources :sales do
39 | # get 'recent', :on => :collection
40 | # end
41 | # end
42 |
43 | # Sample resource route within a namespace:
44 | # namespace :admin do
45 | # # Directs /admin/products/* to Admin::ProductsController
46 | # # (app/controllers/admin/products_controller.rb)
47 | # resources :products
48 | # end
49 |
50 | # You can have the root of your site routed with "root"
51 | # just remember to delete public/index.html.
52 | # root :to => 'welcome#index'
53 |
54 | # See how all your routes lay out with "rake routes"
55 |
56 | # This is a legacy wild controller route that's not recommended for RESTful applications.
57 | # Note: This route will make all actions in every controller accessible via GET requests.
58 | # match ':controller(/:action(/:id(.:format)))'
59 | end
60 |
--------------------------------------------------------------------------------
/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 content = $(assoc + '_fields_blueprint').innerHTML; // Fields template
6 |
7 | // Make the context correct by replacing new_ 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][new_1255929127459][assignments_attributes][new_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(/(new_)?[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 | content = content.replace(
26 | new RegExp('(\\[' + parent_names[i] + '\\])\\[.+?\\]', 'g'),
27 | '$1[' + parent_ids[i] + ']');
28 | }
29 | }
30 | }
31 |
32 | // Make a unique ID for the new child
33 | var regexp = new RegExp('new_' + assoc, 'g');
34 | var new_id = new Date().getTime();
35 | content = content.replace(regexp, "new_" + new_id);
36 |
37 | el.insert({ before:content });
38 | return false;
39 | }
40 | });
41 |
42 | document.observe('click', function (e, el) {
43 | if (el = e.findElement('form a.remove_nested_fields')) {
44 | var hidden_field = el.previous(0);
45 | if (hidden_field) {
46 | hidden_field.value = '1';
47 | }
48 | el.up('.fields').hide();
49 | return false;
50 | }
51 | });
52 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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).attr('data-association'); // Name of child
12 | var content = $('#' + assoc + '_fields_blueprint').html(); // Fields template
13 |
14 | // Make the context correct by replacing new_ with the generated ID
15 | // of each of the parent objects
16 | var context = ($(link).closest('.fields').find('input:first, textarea:first').attr('name') || '').replace(new RegExp('\[[a-z]+\]$'), '');
17 |
18 | // context will be something like this for a brand new form:
19 | // project[tasks_attributes][new_1255929127459][assignments_attributes][new_1255929128105]
20 | // or for an edit form:
21 | // project[tasks_attributes][0][assignments_attributes][1]
22 | if (context) {
23 | var parentNames = context.match(/[a-z_]+_attributes/g) || [];
24 | var parentIds = context.match(/(new_)?[0-9]+/g) || [];
25 |
26 | for(var i = 0; i < parentNames.length; i++) {
27 | if(parentIds[i]) {
28 | content = content.replace(
29 | new RegExp('(_' + parentNames[i] + ')_.+?_', 'g'),
30 | '$1_' + parentIds[i] + '_');
31 |
32 | content = content.replace(
33 | new RegExp('(\\[' + parentNames[i] + '\\])\\[.+?\\]', 'g'),
34 | '$1[' + parentIds[i] + ']');
35 | }
36 | }
37 | }
38 |
39 | // Make a unique ID for the new child
40 | var regexp = new RegExp('new_' + assoc, 'g');
41 | var new_id = new Date().getTime();
42 | content = content.replace(regexp, "new_" + new_id);
43 |
44 | var field = this.insertFields(content, assoc, link);
45 | $(link).closest("form")
46 | .trigger({ type: 'nested:fieldAdded', field: field })
47 | .trigger({ type: 'nested:fieldAdded:' + assoc, field: field });
48 | return false;
49 | },
50 | insertFields: function(content, assoc, link) {
51 | return $(content).insertBefore(link);
52 | },
53 | removeFields: function(e) {
54 | var link = e.currentTarget;
55 | var hiddenField = $(link).prev('input[type=hidden]');
56 | hiddenField.val('1');
57 | // if (hiddenField) {
58 | // $(link).v
59 | // hiddenField.value = '1';
60 | // }
61 | var field = $(link).closest('.fields');
62 | field.hide();
63 | $(link).closest("form").trigger({ type: 'nested:fieldRemoved', field: field });
64 | return false;
65 | }
66 | };
67 |
68 | window.nestedFormEvents = new NestedFormEvents();
69 | $('form a.add_nested_fields').live('click', nestedFormEvents.addFields);
70 | $('form a.remove_nested_fields').live('click', nestedFormEvents.removeFields);
71 | })(jQuery);
72 |
--------------------------------------------------------------------------------
/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 | describe "with no options" do
6 | before(:each) do
7 | @project = Project.new
8 | @template = ActionView::Base.new
9 | @template.output_buffer = ""
10 | @builder = builder.new(:item, @project, @template, {}, proc {})
11 | end
12 |
13 | it "has an add link which behaves similar to a Rails link_to" do
14 | @builder.link_to_add("Add", :tasks).should == 'Add'
15 | @builder.link_to_add("Add", :tasks, :class => "foo", :href => "url").should == 'Add'
16 | @builder.link_to_add(:tasks) { "Add" }.should == 'Add'
17 | end
18 |
19 | it "has a remove link which behaves similar to a Rails link_to" do
20 | @builder.link_to_remove("Remove").should == 'Remove'
21 | @builder.link_to_remove("Remove", :class => "foo", :href => "url").should == 'Remove'
22 | @builder.link_to_remove { "Remove" }.should == 'Remove'
23 | end
24 |
25 | it "should wrap nested fields each in a div with class" do
26 | 2.times { @project.tasks.build }
27 | @builder.fields_for(:tasks) do
28 | "Task"
29 | end.should == '
Task
Task
'
30 | end
31 |
32 | it "should wrap nested fields with specified tag" do
33 | 2.times { @project.tasks.build }
34 | @builder.fields_for(:tasks, :wrapper_tag => 'tr') do
35 | "
Task
".html_safe
36 | end.should == '
Task
Task
'
37 | end
38 |
39 | it "should add class to nested fields wrapper" do
40 | 2.times { @project.tasks.build }
41 | @builder.fields_for(:tasks, :wrapper_class => 'task') do
42 | "Task"
43 | end.should == '
Task
Task
'
44 | end
45 |
46 | it "should add class to nested fields wrapper" do
47 | tasks = []
48 | 2.times { tasks << @project.tasks.new }
49 | @builder.fields_for(:tasks, tasks, :wrapper_class => 'task') do
50 | "Task"
51 | end.should == '
Task
Task
'
52 | end
53 |
54 | it "should add task fields to hidden div after form" do
55 | pending
56 | output = ""
57 | mock(@template).after_nested_form(:tasks) { |arg, block| output << block.call }
58 | @builder.fields_for(:tasks) { "Task" }
59 | @builder.link_to_add("Add", :tasks)
60 | output.should == '
Task
'
61 | end
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/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 | # See the README for more details on where to call this method.
14 | def link_to_add(*args, &block)
15 | options = args.extract_options!.symbolize_keys
16 | association = args.pop
17 | options[:class] = [options[:class], "add_nested_fields"].compact.join(" ")
18 | options["data-association"] = association
19 | args << (options.delete(:href) || "javascript:void(0)")
20 | args << options
21 | @fields ||= {}
22 | @template.after_nested_form(association) do
23 | model_object = object.class.reflect_on_association(association).klass.new
24 | blueprint = fields_for(association, model_object, :child_index => "new_#{association}", :wrapper_tag => @fields[association][:wrapper_tag], :wrapper_class => @fields[association][:wrapper_class], &@fields[association][:block])
25 | blueprint_options = { :type => 'text/html', :id => "#{association}_fields_blueprint" }
26 | @template.content_tag(:script, blueprint, blueprint_options)
27 | end
28 | @template.link_to(*args, &block)
29 | end
30 |
31 | # Adds a link to remove the associated record. The first argment is the name of the link.
32 | #
33 | # f.link_to_remove("Remove Task")
34 | #
35 | # You can pass HTML options in a hash at the end and a block for the content.
36 | #
37 | # <%= f.link_to_remove(:class => "remove_task", :href => "#") do %>
38 | # Remove Task
39 | # <% end %>
40 | #
41 | # See the README for more details on where to call this method.
42 | def link_to_remove(*args, &block)
43 | options = args.extract_options!.symbolize_keys
44 | options[:class] = [options[:class], "remove_nested_fields"].compact.join(" ")
45 | args << (options.delete(:href) || "javascript:void(0)")
46 | args << options
47 | (hidden_field(:_destroy) << @template.link_to(*args, &block)).html_safe
48 | end
49 |
50 | def fields_for_with_nested_attributes(association_name, *args)
51 | # TODO Test this better
52 | block = args.pop || Proc.new { |fields| @template.render(:partial => "#{association_name.to_s.singularize}_fields", :locals => {:f => fields}) }
53 |
54 | convert = false
55 | if Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new("3.1.0") # 3.0.x
56 | options = args[0].extract_options!
57 | convert = true
58 | else # >= 3.1.0
59 | options = args.extract_options!
60 | end
61 |
62 | options[:wrapper_tag] ||= 'div'
63 | options[:wrapper_class] = ' ' << options[:wrapper_class] if options[:wrapper_class]
64 |
65 | if convert
66 | args[0] << options
67 | else
68 | args << options
69 | end
70 |
71 | @fields ||= {}
72 | @fields[association_name] = { :block => block, :wrapper_tag => options[:wrapper_tag], :wrapper_class => options[:wrapper_class] }
73 | super(association_name, *(args << block))
74 | end
75 |
76 | def fields_for_nested_model(name, object, options, block)
77 | @template.content_tag(options[:wrapper_tag], super, :class => "fields#{options[:wrapper_class]}")
78 | end
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/README.rdoc:
--------------------------------------------------------------------------------
1 | = Nested Form
2 |
3 | {}[http://travis-ci.org/fxposter/nested_form]
4 |
5 | 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.
6 |
7 | 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.
8 |
9 | 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].
10 |
11 |
12 | === Enhanced jQuery javascript template
13 |
14 | Now you can override default behavior of inserting new subforms into your form (jQuery-only feature, sorry). For example:
15 |
16 | window.nestedFormEvents.insertFields = function(content, assoc, link) {
17 | return $(link).closest('form').find(assoc + '_fields').append($(content));
18 | }
19 |
20 |
21 | === Rails 3.1 support (with asset pipeline)
22 |
23 | There's a small fix for Rails 3.1 form handling.
24 | Also support of Rails 3.1 led me to huge refactor of specs setup (now using Helper specs from rspec-rails instead of custom implementation spec context).
25 |
26 | Formtastic is supported only with Rails 3.0, Rails 3.1 integration is not available now.
27 |
28 | Asset pipeline is supported. So you can use
29 |
30 | //= require jquery_nested_form
31 | // or
32 | //= require prototype_nested_form
33 |
34 |
35 | == Setup
36 |
37 | Add it to your Gemfile then run +bundle+ to install it.
38 |
39 | gem "nested_form"
40 |
41 | Next run the generator to create the JavaScript file. This will automatically detect if you are using jQuery or Prototype.
42 |
43 | rails g nested_form:install
44 |
45 | Running the generator will add a file at public/javascripts/nested_form.js which should be included after the jQuery or Prototype framework.
46 |
47 | <%= javascript_include_tag :defaults, "nested_form" %>
48 |
49 |
50 | == Usage
51 |
52 | 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 don't have the accepts_nested_attributes_for :tasks you'll get a Missing Block Error.
53 |
54 | This will create a tasks_attributes= method, so you may need to add it to the attr_accessible array. (attr_accessible :tasks_attributes)
55 |
56 | Then use the +nested_form_for+ helper method to enable the nesting.
57 |
58 | <%= nested_form_for @project do |f| %>
59 |
60 | 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.
61 |
62 | <%= f.fields_for :tasks do |task_form| %>
63 | <%= task_form.text_field :name %>
64 | <%= task_form.link_to_remove "Remove this task" %>
65 | <% end %>
66 |
<%= f.link_to_add "Add a task", :tasks %>
67 |
68 | By default fields_for inside nested_form_for will add
wrapper for every nested object. Sometimes you need not the
, but the
(if you insert rows into table). Now you can specify wrapping tag by providing :wrapper_tag => 'tr' option (by default it is 'div'):
69 |
70 |
80 |
81 | You should specify id field, cause form_for will insert it between
-s and this is not pretty good.
82 |
83 | Also, you can specify :wrapper_class, which will be added "fields" class in wrapper element:
84 |
85 | <%= f.fields_for :tasks, :wrapper_class => 'task' do |task_form| %>
86 | <%= task_form.text_field :name %>
87 | <%= task_form.link_to_remove "Remove this task" %>
88 | <% end %>
89 |
<%= f.link_to_add "Add a task", :tasks %>
90 |
91 |
92 | == SimpleForm and Formtastic Support
93 |
94 | Use simple_nested_form_for or semantic_nested_form_for for SimpleForm and Formtastic support respectively. This is feature is not yet in a Gem release but is in the Git repo.
95 |
96 |
97 | == Partials
98 |
99 | 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.
100 |
101 | <%= f.fields_for :tasks %>
102 |
103 | In this case it will look for a partial called "task_fields" and pass the form builder as an +f+ variable to it.
104 |
105 |
106 | == Events
107 |
108 | If you are using jQuery, nested:fieldAdded and nested:fieldRemoved events are triggered on the +form+ element after adding and removing fields.
109 |
110 |
111 | == Special Thanks
112 |
113 | 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].
114 |
115 | 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.
116 |
117 | 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.
118 |
119 | Thanks {Pavel Forkert}[https://github.com/fxposter] for the SimpleForm and Formtastic support.
120 |
--------------------------------------------------------------------------------