├── .gitignore ├── .rspec ├── .travis.yml ├── Appraisals ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── gemfiles ├── rails_5.2.gemfile └── rails_6.0.gemfile ├── lib ├── assets │ └── javascripts │ │ └── nested_form_fields.js.coffee ├── nested_form_fields.rb └── nested_form_fields │ └── version.rb ├── nested_form_fields.gemspec └── spec ├── dummy ├── .sass-cache │ └── b3239b16cb7e494d5d4e969f1b84625be9aac82c │ │ ├── application.css.scssc │ │ ├── normalize.css.scssc │ │ └── users.css.scssc ├── README.rdoc ├── Rakefile ├── app │ ├── assets │ │ ├── config │ │ │ └── manifest.js │ │ ├── images │ │ │ └── .keep │ │ ├── javascripts │ │ │ ├── application.js │ │ │ └── users.js.coffee │ │ └── stylesheets │ │ │ ├── application.css.scss │ │ │ ├── normalize.css.scss │ │ │ └── users.css.scss │ ├── controllers │ │ ├── application_controller.rb │ │ └── users_controller.rb │ ├── models │ │ ├── project.rb │ │ ├── todo.rb │ │ └── user.rb │ └── views │ │ ├── layouts │ │ ├── _messages.html.haml │ │ ├── _navigation.html.haml │ │ └── application.html.haml │ │ └── users │ │ ├── edit.html.haml │ │ └── show.html.haml ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── backtrace_silencers.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ ├── secret_token.rb │ │ ├── session_store.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ └── en.yml │ └── routes.rb ├── db │ ├── development.sqlite3 │ ├── migrate │ │ ├── 20120518212100_create_users.rb │ │ ├── 20120523095218_create_projects.rb │ │ └── 20120523095357_create_todos.rb │ ├── schema.rb │ └── test.sqlite3 ├── lib │ └── assets │ │ └── .gitkeep ├── log │ ├── .gitkeep │ └── development.log ├── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ └── favicon.ico └── script │ └── rails ├── integration └── nested_form_fields_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | .idea 19 | .sass-cache 20 | spec/dummy/log 21 | spec/dummy/tmp 22 | .DS_Store 23 | gemfiles/*.lock 24 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | cache: bundler 3 | sudo: false 4 | gemfile: 5 | - gemfiles/rails_5.2.gemfile 6 | - gemfiles/rails_6.0.gemfile 7 | rvm: 8 | - 2.3.8 9 | - 2.4.5 10 | - 2.5.3 11 | - 2.6.5 12 | - 2.7.0 13 | services: 14 | - xvfb 15 | script: 16 | - bundle exec rspec 17 | matrix: 18 | exclude: 19 | - rvm: 2.3.8 20 | gemfile: gemfiles/rails_6.0.gemfile 21 | - rvm: 2.4.5 22 | gemfile: gemfiles/rails_6.0.gemfile 23 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | appraise 'rails-5.2' do 4 | gem 'activeresource' 5 | gem 'haml' 6 | gem 'haml-rails' 7 | gem 'puma' 8 | gem 'rails', '~> 5.2.4.1' 9 | gem 'sassc-rails' 10 | gem 'sqlite3' 11 | end 12 | 13 | appraise 'rails-6.0' do 14 | gem 'activeresource' 15 | gem 'haml' 16 | gem 'haml-rails' 17 | gem 'puma' 18 | gem 'rails', '~> 6.0.2.1' 19 | gem 'sassc-rails' 20 | gem 'sqlite3' 21 | end 22 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in nested_form_fields.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Nico Ritsche 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nested Form Fields [![Build Status](https://secure.travis-ci.org/ncri/nested_form_fields.png)](http://travis-ci.org/ncri/nested_form_fields) 2 | 3 | This Rails gem helps creating forms for models with nested has_many associations. 4 | 5 | It uses jQuery to dynamically add and remove nested associations. 6 | 7 | - Works for arbitrarily deeply nested associations (tested up to 4 levels). 8 | - Works with form builders like [simple_form](https://github.com/plataformatec/simple_form). 9 | - Requires Ruby 1.9+ and the Rails asset pipeline. 10 | 11 | 12 | 13 | ## Installation 14 | 15 | Add this line to your application's Gemfile: 16 | 17 | ```ruby 18 | gem 'nested_form_fields' 19 | ``` 20 | 21 | And then execute: 22 | 23 | ```console 24 | $ bundle 25 | ``` 26 | 27 | In your application.js file add: 28 | 29 | ```javascript 30 | //= require nested_form_fields 31 | ``` 32 | 33 | ### Rails 5.1+ 34 | 35 | You will need to install jQuery as Rails dropped it from its default stack. 36 | 37 | Add to Gemfile: 38 | 39 | ```ruby 40 | gem 'jquery-rails' 41 | ``` 42 | 43 | Execute: 44 | 45 | ```console 46 | $ bundle 47 | ``` 48 | 49 | Add to application.js: 50 | 51 | ```javascript 52 | //= require jquery3 53 | //= require jquery_ujs 54 | ``` 55 | 56 | ## Usage 57 | 58 | Assume you have a user model with nested videos: 59 | 60 | ```ruby 61 | class User < ActiveRecord::Base 62 | has_many :videos 63 | accepts_nested_attributes_for :videos, allow_destroy: true 64 | end 65 | ``` 66 | 67 | Use the `nested_fields_for` helper inside your user form to add the video fields: 68 | 69 | ```haml 70 | = form_for @user do |f| 71 | = f.nested_fields_for :videos do |ff| 72 | = ff.text_field :video_title 73 | .. 74 | ``` 75 | 76 | Links to add and remove fields can be added using the `add_nested_fields_link` and `remove_nested_fields_link` helpers: 77 | 78 | ```haml 79 | = form_for @user do |f| 80 | = f.nested_fields_for :videos do |ff| 81 | = ff.remove_nested_fields_link 82 | = ff.text_field :video_title 83 | .. 84 | = f.add_nested_fields_link :videos 85 | ``` 86 | 87 | Note that `remove_nested_fields_link` needs to be called within the `nested_fields_for` call and `add_nested_fields_link` outside of it via the parent builder. 88 | 89 | ## Link Customization 90 | 91 | You can change the link text of `remove_nested_fields_link` and `add_nested_fields_link` like this: 92 | 93 | ```haml 94 | ... 95 | ff.remove_nested_fields_link 'Remove me' 96 | ... 97 | f.add_nested_fields_link :videos, 'Add another funtastic video' 98 | ``` 99 | 100 | You can add classes/attributes to the `remove_nested_fields_link` and `add_nested_fields_link` like this: 101 | 102 | ```haml 103 | ... 104 | ff.remove_nested_fields_link 'Remove me', class: 'btn btn-danger', role: 'button' 105 | ... 106 | f.add_nested_fields_link :videos, 'Add another funtastic video', class: 'btn btn-primary', role: 'button' 107 | ``` 108 | 109 | You can supply a block to the `remove_nested_fields_link` and the `add_nested_fields_link` helpers, as you can with `link_to`: 110 | 111 | ```haml 112 | = ff.remove_nested_fields_link do 113 | Remove me %span.icon-trash 114 | ``` 115 | 116 | You can add a `data-confirm` attribute to the `remove_nested_fields_link` if you want the user to confirm whenever they remove a nested field: 117 | 118 | ```haml 119 | = ff.remove_nested_fields_link 'Remove me', data: { confirm: 'Are you sure?' } 120 | ``` 121 | 122 | ## Custom Container 123 | 124 | You can specify a custom container to add nested forms into, by supplying an id via the `data-insert-into` attribute of the `add_nested_fields_link`: 125 | 126 | ```haml 127 | f.add_nested_fields_link :videos, 'Add another funtastic video', data: { insert_into: '' } 128 | ``` 129 | 130 | ## Custom Fields Wrapper 131 | 132 | You can change the type of the element wrapping the nested fields using the `wrapper_tag` option: 133 | 134 | ```haml 135 | = f.nested_fields_for :videos, wrapper_tag: :div do |ff| 136 | ``` 137 | 138 | The default wrapper element is a fieldset. To add legend element to the fieldset use: 139 | 140 | ```haml 141 | = f.nested_fields_for :videos, legend: "Video" do |ff| 142 | ``` 143 | 144 | You can pass options like you would to the `content_tag` method by nesting them in a `:wrapper_options` hash: 145 | 146 | ```haml 147 | = f.nested_fields_for :videos, wrapper_options: { class: 'row' } do |ff| 148 | ``` 149 | 150 | ## Rails 4 Parameter Whitelisting 151 | 152 | If you are using Rails 4 remember to add {{ NESTED_MODEL }}_attributes and the attributes to the permitted params. 153 | If you want to destroy the nested model you should add `:_destroy` and `:id`. 154 | For example: 155 | 156 | ```haml 157 | # app/views/users/_form.haml.erb 158 | = form_for @user do |f| 159 | = f.nested_fields_for :videos do |ff| 160 | = ff.remove_nested_fields_link 161 | = ff.text_field :video_title 162 | .. 163 | = f.add_nested_fields_link :videos 164 | ``` 165 | 166 | ```ruby 167 | # app/controllers/users_controller 168 | .. 169 | def user_params 170 | params.require(:user) 171 | .permit(:name,:email,videos_attributes:[:video_title,:_destroy,:id]) 172 | # ^^^ ^^^ ^^^ 173 | # nested model attrs 174 | # they will let you delete the nested model 175 | end 176 | ``` 177 | 178 | ## Events 179 | 180 | There are four JavaScript events firing before and after addition/removal of the fields in the `nested_form_fields` namespace: 181 | 182 | - `fields_adding` 183 | - `fields_added` 184 | - `fields_removing` 185 | - `fields_removed` 186 | 187 | The events `fields_added` and `fields_removed` are triggered on the element being added or removed. The events bubble up so you can listen for them on any parent element. 188 | This makes it easy to add listeners when you have multiple `nested_form_fields` on the same page. 189 | 190 | CoffeeScript samples: 191 | 192 | ```coffeescript 193 | # Listen on an element 194 | initializeSortable -> ($el) 195 | $el.sortable(...) 196 | $el.on 'fields_added.nested_form_fields', (event, param) -> 197 | console.log event.target # The added field 198 | console.log $(this) # $el 199 | 200 | # Listen on document 201 | $(document).on "fields_added.nested_form_fields", (event, param) -> 202 | switch param.object_class 203 | when "video" 204 | console.log "Video object added" 205 | else 206 | console.log "INFO: Fields were successfully added, callback not handled." 207 | ``` 208 | 209 | You can pass any additional data to the event's callback. This may be useful if you trigger them programmatically. Example: 210 | 211 | ```coffeescript 212 | # Trigger button click programmatically and pass an object `{hello: 'world'}` 213 | $('.add_nested_fields_link').trigger('click', [{hello: 'world'}]) 214 | 215 | # Listen for the event 216 | $(document).on "fields_added.nested_form_fields", (event, param) -> 217 | console.log param.additional_data #=> {hello: 'world'} 218 | ``` 219 | 220 | ## Index replacement string 221 | 222 | Sometimes your code needs to know what index it has when it is instantiated onto the page. 223 | HTML data elements may need to point to other form elements for instance. This is needed for integration 224 | with rails3-jquery-autocomplete. 225 | 226 | To enable string substitution with the current index use the magic string `__nested_field_for_replace_with_index__`. 227 | 228 | ## Contributing 229 | 230 | 1. Fork it 231 | 2. Create your feature branch (`git checkout -b my-new-feature`) 232 | 3. Commit your changes (`git commit -am 'Added some feature'`) 233 | 4. Push to the branch (`git push origin my-new-feature`) 234 | 5. Create new Pull Request 235 | 236 | 237 | ## Contributers 238 | 239 | https://github.com/ncri/nested_form_fields/graphs/contributors 240 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | task default: :spec 8 | -------------------------------------------------------------------------------- /gemfiles/rails_5.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activeresource" 6 | gem "haml" 7 | gem "haml-rails" 8 | gem "puma" 9 | gem "rails", "~> 5.2.4.1" 10 | gem "sassc-rails" 11 | gem "sqlite3" 12 | 13 | gemspec path: "../" 14 | -------------------------------------------------------------------------------- /gemfiles/rails_6.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activeresource" 6 | gem "haml" 7 | gem "haml-rails" 8 | gem "puma" 9 | gem "rails", "~> 6.0.2.1" 10 | gem "sassc-rails" 11 | gem "sqlite3" 12 | 13 | gemspec path: "../" 14 | -------------------------------------------------------------------------------- /lib/assets/javascripts/nested_form_fields.js.coffee: -------------------------------------------------------------------------------- 1 | window.nested_form_fields or= {} 2 | 3 | nested_form_fields.bind_nested_forms_links = () -> 4 | $('body').off("click", '.add_nested_fields_link') 5 | $('body').on 'click', '.add_nested_fields_link', (event, additional_data) -> 6 | $link = $(this) 7 | object_class = $link.data('object-class') 8 | association_path = $link.data('association-path') 9 | added_index = $(".nested_#{association_path}").length 10 | $.event.trigger("fields_adding.nested_form_fields",{object_class: object_class, added_index: added_index, association_path: association_path, additional_data: additional_data}); 11 | if $link.data('scope') 12 | $template = $("#{$link.data('scope')} ##{association_path}_template") 13 | else 14 | $template = $("##{association_path}_template") 15 | target = $link.data('insert-into') 16 | 17 | template_html = $template.html() 18 | 19 | # insert association indexes 20 | index_placeholder = "__#{association_path}_index__" 21 | template_html = template_html.replace(new RegExp(index_placeholder,"g"), added_index) 22 | # look for replacements in user defined code and substitute with the index 23 | template_html = template_html.replace(new RegExp("__nested_field_for_replace_with_index__","g"), added_index) 24 | 25 | # replace child template div tags with script tags to avoid form submission of templates 26 | $parsed_template = $(template_html) 27 | $child_templates = $parsed_template.closestChild('.form_template') 28 | $child_templates.each () -> 29 | $child = $(this) 30 | $child.replaceWith($("