├── .rspec ├── spec ├── dummy │ ├── app │ │ ├── mailers │ │ │ └── .gitkeep │ │ ├── models │ │ │ ├── .gitkeep │ │ │ ├── milestone.rb │ │ │ ├── project.rb │ │ │ └── task.rb │ │ ├── helpers │ │ │ ├── projects_helper.rb │ │ │ └── application_helper.rb │ │ ├── assets │ │ │ ├── javascripts │ │ │ │ ├── jquery_nested_form.js │ │ │ │ ├── prototype_nested_form.js │ │ │ │ ├── projects.js │ │ │ │ └── application.js │ │ │ └── stylesheets │ │ │ │ ├── projects.css │ │ │ │ └── application.css │ │ ├── controllers │ │ │ ├── application_controller.rb │ │ │ └── projects_controller.rb │ │ └── views │ │ │ ├── layouts │ │ │ └── application.html.erb │ │ │ └── projects │ │ │ └── new.html.erb │ ├── public │ │ ├── favicon.ico │ │ ├── javascripts │ │ ├── 422.html │ │ ├── 404.html │ │ └── 500.html │ ├── tmp │ │ └── cache │ │ │ └── .gitkeep │ ├── test │ │ ├── unit │ │ │ └── helpers │ │ │ │ └── projects_helper_test.rb │ │ └── functional │ │ │ └── projects_controller_test.rb │ ├── config.ru │ ├── config │ │ ├── environment.rb │ │ ├── locales │ │ │ └── en.yml │ │ ├── initializers │ │ │ ├── mime_types.rb │ │ │ ├── inflections.rb │ │ │ ├── backtrace_silencers.rb │ │ │ ├── session_store.rb │ │ │ ├── secret_token.rb │ │ │ └── wrap_parameters.rb │ │ ├── boot.rb │ │ ├── database.yml │ │ ├── environments │ │ │ ├── development.rb │ │ │ ├── test.rb │ │ │ └── production.rb │ │ ├── application.rb │ │ └── routes.rb │ ├── Rakefile │ ├── script │ │ └── rails │ └── db │ │ ├── migrate │ │ └── 20110710143903_initial_tables.rb │ │ └── schema.rb ├── spec_helper.rb ├── form_spec.rb └── nested_form │ ├── view_helper_spec.rb │ └── builder_spec.rb ├── .rvmrc ├── lib ├── nested_form.rb ├── nested_form │ ├── engine.rb │ ├── builders.rb │ ├── view_helper.rb │ └── builder_mixin.rb └── generators │ └── nested_form │ └── install_generator.rb ├── gemfiles ├── Gemfile.rails3_0 ├── Gemfile.rails3_1 └── Gemfile.base ├── .gitignore ├── Gemfile ├── .travis.yml ├── nested_form.gemspec ├── LICENSE ├── Rakefile ├── CHANGELOG.rdoc ├── vendor └── assets │ └── javascripts │ ├── prototype_nested_form.js │ └── jquery_nested_form.js └── README.rdoc /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /spec/dummy/app/mailers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/tmp/cache/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm use 1.9.3@nested_form --create 2 | -------------------------------------------------------------------------------- /lib/nested_form.rb: -------------------------------------------------------------------------------- 1 | require "nested_form/engine" 2 | -------------------------------------------------------------------------------- /spec/dummy/public/javascripts: -------------------------------------------------------------------------------- 1 | ../app/assets/javascripts -------------------------------------------------------------------------------- /spec/dummy/app/helpers/projects_helper.rb: -------------------------------------------------------------------------------- 1 | module ProjectsHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy/app/models/milestone.rb: -------------------------------------------------------------------------------- 1 | class Milestone < ActiveRecord::Base 2 | belongs_to :task 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/jquery_nested_form.js: -------------------------------------------------------------------------------- 1 | ../../../../../vendor/assets/javascripts/jquery_nested_form.js -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/prototype_nested_form.js: -------------------------------------------------------------------------------- 1 | ../../../../../vendor/assets/javascripts/prototype_nested_form.js -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails3_0: -------------------------------------------------------------------------------- 1 | instance_eval File.read(File.expand_path('../Gemfile.base', __FILE__)) 2 | gem "rails", "~> 3.0.0" 3 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.rails3_1: -------------------------------------------------------------------------------- 1 | instance_eval File.read(File.expand_path('../Gemfile.base', __FILE__)) 2 | gem "rails", "~> 3.1.0" 3 | -------------------------------------------------------------------------------- /spec/dummy/test/unit/helpers/projects_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ProjectsHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/models/project.rb: -------------------------------------------------------------------------------- 1 | class Project < ActiveRecord::Base 2 | has_many :tasks 3 | accepts_nested_attributes_for :tasks 4 | end 5 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | 6 | 7 | 8 | <%= yield %> 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.base: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | gem 'activerecord-jdbcsqlite3-adapter', :platforms => :jruby 3 | gem "sqlite3", :platforms => :ruby 4 | gem "simple_form" 5 | gem "formtastic" 6 | gem "rake" 7 | gem "capybara" 8 | gemspec :path => '../' 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gemspec 4 | 5 | gem 'activerecord-jdbcsqlite3-adapter', :platforms => :jruby 6 | gem 'sqlite3', :platforms => :ruby 7 | gem 'simple_form' 8 | gem 'formtastic' 9 | gem 'rake' 10 | gem 'capybara' 11 | gem 'rails', '~> 3.2.0' 12 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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 | end 24 | -------------------------------------------------------------------------------- /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/app/views/projects/new.html.erb: -------------------------------------------------------------------------------- 1 | <% if params[:type] == 'prototype' %> 2 | <%= javascript_include_tag 'prototype', 'prototype_nested_form' %> 3 | <% else %> 4 | <%= javascript_include_tag 'jquery', 'jquery_nested_form' %> 5 | <% end %> 6 | 7 | <%= nested_form_for @project do |f| -%> 8 | <%= f.text_field :name %> 9 | <%= f.fields_for :tasks do |tf| -%> 10 | <%= tf.text_field :name %> 11 | <%= tf.fields_for :milestones do |mf| %> 12 | <%= mf.text_field :name %> 13 | <%= mf.link_to_remove 'Remove milestone' %> 14 | <% end %> 15 | <%= tf.link_to_add 'Add new milestone', :milestones %> 16 | <%= tf.link_to_remove 'Remove' %> 17 | <% end -%> 18 | <%= f.link_to_add 'Add new task', :tasks %> 19 | <% end -%> 20 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 == 'TaskTask' 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 == '' 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 | 71 | <%= f.fields_for :tasks, :wrapper_tag => 'tr' do |task_form| %> 72 | 77 | <% end %> 78 |
73 | <%= task_form.hidden_field :id %> 74 | <%= task_form.text_field :name %> 75 | <%= task_form.link_to_remove "Remove this task" %> 76 |
79 |

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

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 | --------------------------------------------------------------------------------