├── Gemfile ├── .gitignore ├── lib ├── awesome_nested_fields │ ├── version.rb │ ├── engine.rb │ └── railtie.rb ├── generators │ └── awesome_nested_fields │ │ └── install │ │ └── install_generator.rb ├── awesome_nested_fields.rb └── rails │ └── form_helper.rb ├── test ├── test_helper.rb ├── generators │ └── install_generator_test.rb └── awesome_nested_fields_test.rb ├── Rakefile ├── awesome_nested_fields.gemspec ├── LICENSE ├── Gemfile.lock ├── vendor └── assets │ └── javascripts │ ├── jquery.nested-fields.min.js │ └── jquery.nested-fields.js └── README.md /Gemfile: -------------------------------------------------------------------------------- 1 | source :gemcutter 2 | gemspec 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/* 2 | *.gem 3 | .bundle 4 | demos/*/** 5 | -------------------------------------------------------------------------------- /lib/awesome_nested_fields/version.rb: -------------------------------------------------------------------------------- 1 | module AwesomeNestedFields 2 | VERSION = "0.6.4" 3 | end 4 | -------------------------------------------------------------------------------- /lib/awesome_nested_fields/engine.rb: -------------------------------------------------------------------------------- 1 | module AwesomeNestedFields 2 | class Engine < ::Rails::Engine 3 | 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift 'lib' 2 | 3 | require "rubygems" unless defined?(Gem) 4 | require "rails" 5 | require "awesome_nested_fields" 6 | require "turn" 7 | 8 | require "rails/generators/test_case" 9 | 10 | -------------------------------------------------------------------------------- /lib/awesome_nested_fields/railtie.rb: -------------------------------------------------------------------------------- 1 | module AwesomeNestedFields 2 | class Railtie < ::Rails::Railtie 3 | config.before_configuration do 4 | config.action_view.javascript_expansions[:defaults] << 'jquery.nested-fields' 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/generators/install_generator_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class InstallGeneratorTest < Rails::Generators::TestCase 4 | tests InstallGenerator 5 | destination File.expand_path("../../tmp", __FILE__) 6 | setup :prepare_destination 7 | 8 | test "it creates all files properly" do 9 | run_generator 10 | assert_file "public/javascripts/jquery.nested-fields.js" if Rails.version < "3.1" 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /test/awesome_nested_fields_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AwesomeNestedFieldsTest < ActiveSupport::TestCase 4 | test "it has a version" do 5 | version = AwesomeNestedFields::VERSION 6 | assert_match version, /^\d+\.\d+\.\d+$/ 7 | assert_not_nil version 8 | end 9 | 10 | test "it can escape HTML content" do 11 | escaped_content = AwesomeNestedFields.escape_html_tags("Home > News & Updates") 12 | assert_equal escaped_content, "Home > News & Updates" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | # encoding: UTF-8 5 | 6 | require 'rake/testtask' 7 | require 'rdoc/task' 8 | 9 | task :default => :test 10 | 11 | desc 'Run AwesomeNestedFields unit tests.' 12 | Rake::TestTask.new(:test) do |t| 13 | t.libs << 'lib' 14 | t.libs << 'test' 15 | t.pattern = 'test/*_test.rb' 16 | t.verbose = true 17 | end 18 | 19 | require "rspec/core/rake_task" 20 | RSpec::Core::RakeTask.new(:spec) do |t| 21 | t.rspec_opts = '--backtrace --color' 22 | end 23 | -------------------------------------------------------------------------------- /lib/generators/awesome_nested_fields/install/install_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails' 2 | 3 | module AwesomeNestedFields 4 | module Generators 5 | class InstallGenerator < ::Rails::Generators::Base 6 | desc 'This generator installs Awesome Nested Fields' 7 | source_root File.expand_path('../../../../../vendor/assets/javascripts', __FILE__) 8 | 9 | def copy_js 10 | say_status('copying', 'awesome nested fields js file', :green) 11 | copy_file 'jquery.nested-fields.js', 'public/javascripts/jquery.nested-fields.js' 12 | end 13 | end 14 | end 15 | end if ::Rails.version < '3.1' -------------------------------------------------------------------------------- /lib/awesome_nested_fields.rb: -------------------------------------------------------------------------------- 1 | require "rails" unless defined?(Rails) 2 | require "action_view" unless defined?(ActionView) 3 | 4 | module AwesomeNestedFields 5 | if ::Rails.version < '3.1' 6 | require 'awesome_nested_fields/railtie' 7 | else 8 | require 'awesome_nested_fields/engine' 9 | end 10 | 11 | require 'awesome_nested_fields/version' 12 | 13 | def self.escape_html_tags(html) 14 | html.gsub(/[&><]/) do |char| 15 | case char 16 | when '<' then '<' 17 | when '>' then '>' 18 | when '&' then '&' 19 | end 20 | end.html_safe 21 | end 22 | end 23 | 24 | require 'rails/form_helper' -------------------------------------------------------------------------------- /awesome_nested_fields.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require File.expand_path('../lib/awesome_nested_fields/version', __FILE__) 3 | 4 | Gem::Specification.new do |s| 5 | s.name = 'awesome_nested_fields' 6 | s.version = AwesomeNestedFields::VERSION 7 | s.platform = Gem::Platform::RUBY 8 | s.authors = %q[Lailson Bandeira] 9 | s.email = %q[lailson@guava.com.br] 10 | s.homepage = 'http://rubygems.org/gems/awesome_nested_fields' 11 | s.summary = 'Awesome nested fields for Rails' 12 | s.description = 'Awesome dynamic nested fields for Rails and jQuery' 13 | 14 | s.required_rubygems_version = ">= 1.3.6" 15 | s.rubyforge_project = "awesome_nested_fields" 16 | 17 | s.add_development_dependency 'bundler', '>= 1.0.0' 18 | s.add_development_dependency 'rspec', '>=2' 19 | s.add_development_dependency 'turn', '~> 0.8.3' 20 | s.add_runtime_dependency 'rails', '>= 3.0.0' 21 | 22 | s.files = `git ls-files`.split("\n") 23 | s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact 24 | s.require_path = 'lib' 25 | end 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2013 Lailson Bandeira 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. -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | awesome_nested_fields (0.6.4) 5 | rails (>= 3.0.0) 6 | 7 | GEM 8 | remote: http://rubygems.org/ 9 | specs: 10 | actionmailer (3.2.12) 11 | actionpack (= 3.2.12) 12 | mail (~> 2.4.4) 13 | actionpack (3.2.12) 14 | activemodel (= 3.2.12) 15 | activesupport (= 3.2.12) 16 | builder (~> 3.0.0) 17 | erubis (~> 2.7.0) 18 | journey (~> 1.0.4) 19 | rack (~> 1.4.5) 20 | rack-cache (~> 1.2) 21 | rack-test (~> 0.6.1) 22 | sprockets (~> 2.2.1) 23 | activemodel (3.2.12) 24 | activesupport (= 3.2.12) 25 | builder (~> 3.0.0) 26 | activerecord (3.2.12) 27 | activemodel (= 3.2.12) 28 | activesupport (= 3.2.12) 29 | arel (~> 3.0.2) 30 | tzinfo (~> 0.3.29) 31 | activeresource (3.2.12) 32 | activemodel (= 3.2.12) 33 | activesupport (= 3.2.12) 34 | activesupport (3.2.12) 35 | i18n (~> 0.6) 36 | multi_json (~> 1.0) 37 | ansi (1.4.1) 38 | arel (3.0.2) 39 | builder (3.0.4) 40 | diff-lcs (1.1.3) 41 | erubis (2.7.0) 42 | hike (1.2.1) 43 | i18n (0.6.1) 44 | journey (1.0.4) 45 | json (1.7.7) 46 | mail (2.4.4) 47 | i18n (>= 0.4.0) 48 | mime-types (~> 1.16) 49 | treetop (~> 1.4.8) 50 | mime-types (1.21) 51 | multi_json (1.6.1) 52 | polyglot (0.3.3) 53 | rack (1.4.5) 54 | rack-cache (1.2) 55 | rack (>= 0.4) 56 | rack-ssl (1.3.3) 57 | rack 58 | rack-test (0.6.2) 59 | rack (>= 1.0) 60 | rails (3.2.12) 61 | actionmailer (= 3.2.12) 62 | actionpack (= 3.2.12) 63 | activerecord (= 3.2.12) 64 | activeresource (= 3.2.12) 65 | activesupport (= 3.2.12) 66 | bundler (~> 1.0) 67 | railties (= 3.2.12) 68 | railties (3.2.12) 69 | actionpack (= 3.2.12) 70 | activesupport (= 3.2.12) 71 | rack-ssl (~> 1.3.2) 72 | rake (>= 0.8.7) 73 | rdoc (~> 3.4) 74 | thor (>= 0.14.6, < 2.0) 75 | rake (10.0.3) 76 | rdoc (3.12.1) 77 | json (~> 1.4) 78 | rspec (2.7.0) 79 | rspec-core (~> 2.7.0) 80 | rspec-expectations (~> 2.7.0) 81 | rspec-mocks (~> 2.7.0) 82 | rspec-core (2.7.1) 83 | rspec-expectations (2.7.0) 84 | diff-lcs (~> 1.1.2) 85 | rspec-mocks (2.7.0) 86 | sprockets (2.2.2) 87 | hike (~> 1.2) 88 | multi_json (~> 1.0) 89 | rack (~> 1.0) 90 | tilt (~> 1.1, != 1.3.0) 91 | thor (0.17.0) 92 | tilt (1.3.3) 93 | treetop (1.4.12) 94 | polyglot 95 | polyglot (>= 0.3.1) 96 | turn (0.8.3) 97 | ansi 98 | tzinfo (0.3.35) 99 | 100 | PLATFORMS 101 | ruby 102 | 103 | DEPENDENCIES 104 | awesome_nested_fields! 105 | bundler (>= 1.0.0) 106 | rspec (>= 2) 107 | turn (~> 0.8.3) 108 | -------------------------------------------------------------------------------- /lib/rails/form_helper.rb: -------------------------------------------------------------------------------- 1 | ActionView::Helpers::FormBuilder.class_eval do 2 | 3 | def nested_fields_for(association, options={}, &block) 4 | raise ArgumentError, 'Missing block to nested_fields_for' unless block_given? 5 | 6 | options[:new_item_index] ||= 'new_nested_item' 7 | options[:new_object] ||= self.object.class.reflect_on_association(association).klass.new 8 | options[:item_template_class] ||= ['template', 'item', association.to_s.singularize].join(' ') 9 | options[:empty_template_class] ||= ['template', 'empty', association.to_s.singularize].join(' ') 10 | options[:show_empty] ||= false 11 | options[:render_template] = options.key?(:render_template) ? options[:render_template] : true 12 | options[:escape_template] = options.key?(:escape_template) ? options[:escape_template] : true 13 | 14 | output = @template.capture { fields_for(association, &block) } 15 | output ||= template.raw "" 16 | 17 | if options[:show_empty] and self.object.send(association).empty? 18 | output.safe_concat @template.capture { yield nil } 19 | end 20 | 21 | template = render_nested_fields_template(association, options, &block) 22 | if options[:render_template] 23 | output.safe_concat template 24 | else 25 | add_nested_fields_template(association, template) 26 | end 27 | 28 | output 29 | end 30 | 31 | protected 32 | 33 | def render_nested_fields_template(association, options, &block) 34 | templates = @template.content_tag(:script, :type => 'text/html', :class => options[:item_template_class]) do 35 | template = fields_for(association, options[:new_object], :child_index => options[:new_item_index], &block) 36 | template = AwesomeNestedFields.escape_html_tags(template) if options[:escape_template] 37 | template 38 | end 39 | 40 | if options[:show_empty] 41 | empty_template = @template.content_tag(:script, :type => 'text/html', :class => options[:empty_template_class]) do 42 | template = @template.capture { yield nil } 43 | template = AwesomeNestedFields.escape_html_tags(template) if options[:escape_template] 44 | template 45 | end 46 | templates.safe_concat empty_template 47 | end 48 | 49 | templates 50 | end 51 | 52 | def add_nested_fields_template(association, template) 53 | # It must be a hash, so we don't get repeated templates on deeply nested models 54 | @template.instance_variable_set(:@nested_fields_template_cache, {}) unless @template.instance_variable_get(:@nested_fields_template_cache) 55 | @template.instance_variable_get(:@nested_fields_template_cache)[association] = template 56 | create_nested_fields_template_helper! 57 | end 58 | 59 | def create_nested_fields_template_helper! 60 | def @template.nested_fields_templates 61 | @nested_fields_template_cache.reduce(ActiveSupport::SafeBuffer.new) do |buffer, entry| 62 | association, template = entry 63 | buffer.safe_concat template 64 | end 65 | end unless @template.respond_to?(:nested_fields_templates) 66 | end 67 | 68 | end 69 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/jquery.nested-fields.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Awesome Nested Fields 3 | * 4 | * Requires jquery-ujs adapter. 5 | * https://github.com/lailsonbm/awesome_nested_fields 6 | */ 7 | (function($){"use strict";var defaultSettings={beforeInsert:function(item,callback){callback()},afterInsert:function(item){},beforeRemove:function(item,callback){callback()},afterRemove:function(item){},itemTemplateSelector:".item.template",emptyTemplateSelector:".empty.template",containerSelector:".items, .container",itemSelector:".item",emptySelector:".empty",addSelector:".add",removeSelector:".remove",newItemIndex:"new_nested_item",unescapeTemplate:true};var methods={init:function(options){return this.each(function(){var $this=$(this);if($this.data("nested-fields.options")){log("Nested fields already defined for this element. If you want to redefine options, destroy it and init again.");return $this}options=$.extend({},defaultSettings,options);options.itemTemplate=$(options.itemTemplateSelector,$this);options.emptyTemplate=$(options.emptyTemplateSelector,$this);options.container=$(options.containerSelector,$this);options.add=$(options.addSelector,$this);$this.data("nested-fields.options",options);bindInsertToAdd(options);bindRemoveToItems(options,$this)})},insert:function(callback,options){options=$.extend({},getOptions(this),options);return insertItemWithCallbacks(callback,options)},remove:function(element,options){options=$.extend({},getOptions(this),options);return removeItemWithCallbacks(element,options)},removeAll:function(options){options=$.extend({},getOptions(this),options);$(methods.items.apply(this)).each(function(i,el){methods.remove(el,options)})},items:function(){return findItems(getOptions(this))},destroy:function(){$(this).removeData("nested-fields.options");$("*",this).unbind(".nested-fields")}};$.fn.nestedFields=function(method){if(methods[method]){return methods[method].apply(this,Array.prototype.slice.call(arguments,1))}else if(typeof method==="object"||!method){return methods.init.apply(this,arguments)}else{$.error("Method "+method+" does not exist on jQuery.nestedFields")}};function getOptions(element){var $element=$(element);while($element.length>0){var data=$element.data("nested-fields.options");if(data){return data}else{$element=$element.parent()}}return null}function bindInsertToAdd(options){options.add.bind("click.nested-fields",function(e){e.preventDefault();insertItemWithCallbacks(null,options)})}function bindRemoveToItems(options,$this){$(options.itemSelector,$this).each(function(i,item){bindRemoveToItem(item,options)})}function prepareTemplate(options){var regexp=new RegExp(options.newItemIndex,"g");var newId=(new Date).getTime();var contents=options.itemTemplate.html();if(options["unescapeTemplate"]){contents=unescape_html_tags(contents)}var newItem=$(contents.replace(regexp,newId));newItem.attr("data-new-record",true);newItem.attr("data-record-id",newId);bindRemoveToItem(newItem,options);return newItem}function insertItemWithCallbacks(onInsertCallback,options){var newItem=prepareTemplate(options);function insert(){if(onInsertCallback){onInsertCallback(newItem)}removeEmpty(options);options.container.append(newItem)}if(!options.skipBefore){options.beforeInsert(newItem,insert);if(options.beforeInsert.length<=1){insert()}}else{insert()}if(!options.skipAfter){options.afterInsert(newItem)}return newItem}function removeEmpty(options){findEmpty(options).remove()}function removeItemWithCallbacks(element,options){function remove(){if($element.attr("data-new-record")){$element.remove()}else{$element.find("INPUT[name$='[_destroy]']").val("true");$element.hide()}insertEmpty(options)}var $element=$(element);if(!options.skipBefore){options.beforeRemove($element,remove);if(options.beforeRemove.length<=1){remove()}}else{remove()}if(!options.skipAfter){options.afterRemove($element)}return $element}function insertEmpty(options){if(findItems(options).length===0){var contents=options.emptyTemplate.html();if(contents){if(options["unescapeTemplate"]){contents=unescape_html_tags(contents)}options.container.append(contents)}}}function bindRemoveToItem(item,options){var removeHandler=$(item).find(options.removeSelector);var needsConfirmation=removeHandler.attr("data-confirm");var event=needsConfirmation?"confirm:complete":"click";removeHandler.bind(event+".nested-fields",function(e,confirmed){e.preventDefault();if(confirmed===undefined||confirmed===true){removeItemWithCallbacks(item,options)}return false})}function findItems(options){return options.container.find(options.itemSelector+":visible")}function findEmpty(options){return options.container.find(options.emptySelector)}function unescape_html_tags(html){var e=document.createElement("div");e.innerHTML=html;return e.childNodes.length===0?"":jQuery.trim(e.childNodes[0].nodeValue)}function log(msg){if(console&&console.log){console.log(msg)}}})(jQuery); -------------------------------------------------------------------------------- /vendor/assets/javascripts/jquery.nested-fields.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 'use strict'; 3 | 4 | /** 5 | * 6 | * Awesome Nested Fields 7 | * 8 | * Requires jquery-ujs adapter. 9 | * https://github.com/lailsonbm/awesome_nested_fields 10 | * 11 | */ 12 | 13 | var defaultSettings = { 14 | beforeInsert: function(item, callback) { callback() }, 15 | afterInsert: function(item) {}, 16 | beforeRemove: function(item, callback) { callback() }, 17 | afterRemove: function(item) {}, 18 | itemTemplateSelector: '.item.template', 19 | emptyTemplateSelector: '.empty.template', 20 | containerSelector: '.items, .container', 21 | itemSelector: '.item', 22 | emptySelector: '.empty', 23 | addSelector: '.add', 24 | removeSelector: '.remove', 25 | newItemIndex: 'new_nested_item', 26 | unescapeTemplate: true 27 | }; 28 | 29 | // PUBLIC API 30 | var methods = { 31 | init: function(options) { 32 | return this.each(function() { 33 | var $this = $(this); 34 | if($this.data('nested-fields.options')) { 35 | log('Nested fields already defined for this element. If you want to redefine options, destroy it and init again.'); 36 | return $this; 37 | } 38 | 39 | options = $.extend({}, defaultSettings, options); 40 | options.itemTemplate = $(options.itemTemplateSelector, $this); 41 | options.emptyTemplate = $(options.emptyTemplateSelector, $this); 42 | options.container = $(options.containerSelector, $this); 43 | options.add = $(options.addSelector, $this); 44 | $this.data('nested-fields.options', options); 45 | 46 | bindInsertToAdd(options); 47 | bindRemoveToItems(options, $this); 48 | }); 49 | }, 50 | 51 | insert: function(callback, options) { 52 | options = $.extend({}, getOptions(this), options); 53 | return insertItemWithCallbacks(callback, options); 54 | }, 55 | 56 | remove: function(element, options) { 57 | options = $.extend({}, getOptions(this), options); 58 | return removeItemWithCallbacks(element, options); 59 | }, 60 | 61 | removeAll: function(options) { 62 | options = $.extend({}, getOptions(this), options); 63 | $(methods.items.apply(this)).each(function(i, el) { 64 | methods.remove(el, options); 65 | }); 66 | }, 67 | 68 | items: function() { 69 | return findItems(getOptions(this)); 70 | }, 71 | 72 | destroy: function() { 73 | $(this).removeData('nested-fields.options'); 74 | $('*', this).unbind('.nested-fields'); 75 | } 76 | }; 77 | 78 | $.fn.nestedFields = function(method) { 79 | if (methods[method]) { 80 | return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); 81 | } else if (typeof method === 'object' || !method) { 82 | return methods.init.apply(this, arguments); 83 | } else { 84 | $.error('Method ' + method + ' does not exist on jQuery.nestedFields'); 85 | } 86 | }; 87 | 88 | // Initialization functions 89 | 90 | function getOptions(element) { 91 | var $element = $(element); 92 | while($element.length > 0) { 93 | var data = $element.data('nested-fields.options'); 94 | if(data) { 95 | return data; 96 | } else { 97 | $element = $element.parent(); 98 | } 99 | } 100 | return null; 101 | } 102 | 103 | function bindInsertToAdd(options) { 104 | options.add.bind('click.nested-fields', function(e) { 105 | e.preventDefault(); 106 | insertItemWithCallbacks(null, options); 107 | }); 108 | } 109 | 110 | function bindRemoveToItems(options, $this) { 111 | $(options.itemSelector, $this).each(function(i, item) { 112 | bindRemoveToItem(item, options); 113 | }); 114 | } 115 | 116 | // Insertion functions 117 | 118 | function prepareTemplate(options) { 119 | var regexp = new RegExp(options.newItemIndex, 'g'); 120 | var newId = new Date().getTime(); 121 | 122 | var contents = options.itemTemplate.html(); 123 | if(options['unescapeTemplate']) { 124 | contents = unescape_html_tags(contents); 125 | } 126 | var newItem = $(contents.replace(regexp, newId)); 127 | newItem.attr('data-new-record', true); 128 | newItem.attr('data-record-id', newId); 129 | 130 | bindRemoveToItem(newItem, options); 131 | 132 | return newItem; 133 | } 134 | 135 | function insertItemWithCallbacks(onInsertCallback, options) { 136 | var newItem = prepareTemplate(options); 137 | 138 | function insert() { 139 | if(onInsertCallback) { 140 | onInsertCallback(newItem); 141 | } 142 | removeEmpty(options); 143 | options.container.append(newItem); 144 | } 145 | 146 | if(!options.skipBefore) { 147 | options.beforeInsert(newItem, insert); 148 | if(options.beforeInsert.length <= 1) { 149 | insert(); 150 | } 151 | } else { 152 | insert(); 153 | } 154 | 155 | if(!options.skipAfter) { 156 | options.afterInsert(newItem); 157 | } 158 | 159 | return newItem; 160 | } 161 | 162 | function removeEmpty(options) { 163 | findEmpty(options).remove(); 164 | } 165 | 166 | // Removal functions 167 | 168 | function removeItemWithCallbacks(element, options) { 169 | function remove() { 170 | if($element.attr('data-new-record')) { // record is new 171 | $element.remove(); 172 | } else { // record should be marked and sent to server 173 | $element.find("INPUT[name$='[_destroy]']").val('true'); 174 | $element.hide(); 175 | } 176 | insertEmpty(options); 177 | } 178 | 179 | var $element = $(element); 180 | if(!options.skipBefore) { 181 | options.beforeRemove($element, remove); 182 | if(options.beforeRemove.length <= 1) { 183 | remove(); 184 | } 185 | } else { 186 | remove(); 187 | } 188 | 189 | if(!options.skipAfter) { 190 | options.afterRemove($element); 191 | } 192 | 193 | return $element; 194 | } 195 | 196 | function insertEmpty(options) { 197 | if(findItems(options).length === 0) { 198 | var contents = options.emptyTemplate.html(); 199 | if(contents) { 200 | if(options['unescapeTemplate']) { 201 | contents = unescape_html_tags(contents); 202 | } 203 | options.container.append(contents); 204 | } 205 | } 206 | } 207 | 208 | function bindRemoveToItem(item, options) { 209 | var removeHandler = $(item).find(options.removeSelector); 210 | var needsConfirmation = removeHandler.attr('data-confirm'); 211 | 212 | var event = needsConfirmation ? 'confirm:complete' : 'click'; 213 | removeHandler.bind(event + '.nested-fields', function(e, confirmed) { 214 | e.preventDefault(); 215 | if(confirmed === undefined || confirmed === true) { 216 | removeItemWithCallbacks(item, options); 217 | } 218 | return false; 219 | }); 220 | } 221 | 222 | // Find functions 223 | 224 | function findItems(options) { 225 | return options.container.find(options.itemSelector + ':visible'); 226 | } 227 | 228 | function findEmpty(options) { 229 | return options.container.find(options.emptySelector); 230 | } 231 | 232 | // Utility functions 233 | 234 | function unescape_html_tags(html) { 235 | var e = document.createElement('div'); 236 | e.innerHTML = html; 237 | return e.childNodes.length === 0 ? "" : jQuery.trim(e.childNodes[0].nodeValue); 238 | } 239 | 240 | function log(msg) { 241 | if(console && console.log) { 242 | console.log(msg); 243 | } 244 | } 245 | 246 | })(jQuery); 247 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Deprecation Notice 2 | ===================== 3 | ⚠️ This gem is NOT being mantained anymore. You can use [cocoon](https://github.com/nathanvda/cocoon) instead. 4 | 5 | Awesome Nested Fields [](http://badge.fury.io/rb/awesome_nested_fields) 6 | ===================== 7 | 8 | In Rails, you can create forms that have fields from nested models. For example, if a person has many phone numbers, you can easily create a form that receives data from the person and from a fixed number of phones. However, when you want to allow the person to insert multiple, indefinite phones, you're in trouble: it's [much harder](http://railscasts.com/episodes/196-nested-model-form-part-1) [than it](http://railscasts.com/episodes/197-nested-model-form-part-2) [should be](http://stackoverflow.com/questions/1704142/unobtrusive-dynamic-form-fields-in-rails-with-jquery). Well, not anymore. 9 | 10 | 11 | Installation 12 | ------------ 13 | 14 | ### Rails 3.1 15 | 16 | 1. Add the gem to your Gemfile and run `bundle install` to make sure the gem gets installed. 17 | 18 | ```ruby 19 | gem 'awesome_nested_fields' 20 | ``` 21 | 22 | 2. Add this line to `app/assets/javascripts/application.js` (or where you prefer) so the javascript dependency is added to the asset pipeline. Be sure to include this line after jQuery and jQuery UJS Adapter. 23 | 24 | ```javascript 25 | //= require jquery.nested-fields 26 | ``` 27 | 28 | 3. Rock with your _awesome_ nested models. 29 | 30 | 31 | ### Rails 3.0 32 | 33 | 1. Add the gem to your Gemfile and run `bundle install` to make sure the gem gets installed. Be sure to include it after `jquery-rails` so the javascript files are added in the correct order at the templates. 34 | 35 | ```ruby 36 | gem 'awesome_nested_fields' 37 | ``` 38 | 39 | 2. Copy the javascript dependency to `public/javascripts` by using the generator. 40 | 41 | rails generate awesome_nested_fields:install 42 | 43 | 3. (Optional) The javascript dependency will be added automatically to the defaults javascript files. If you don't use `javascript_include_tag :defaults` in your templates for some reason, require the file manually. 44 | 45 | ```html 46 | 47 | ``` 48 | 49 | 4. Now you're ready to rock with your _awesome_ nested models. It will be so fun as in Rails 3.1, I promise. 50 | 51 | 52 | Basic Usage 53 | ----------- 54 | 55 | ### Model 56 | 57 | First, make sure the object that has the `has_many` or `has_and_belongs_to_many` relation accepts nested attributes for the collection you want. For example, if a person `has_many` phones, we'll have a model like this: 58 | 59 | ```ruby 60 | class Person < ActiveRecord::Base 61 | has_many :phones 62 | accepts_nested_attributes_for :phones, allow_destroy: true 63 | end 64 | ``` 65 | 66 | The `accepts_nested_attributes_for` is a method from Active Record that allows you to pass attributes of nested models directly to its parent, instead of instantiate each child object separately. In this case, `Person` gains a method called `phones_attributes=`, that accepts data for new and existing phones of a given person. The `allow_destroy` option enables us to also delete child objects. To know more about nested attributes, check out the [ActiveRecord::NestedAttribute](https://github.com/rails/rails/blob/master/activerecord/lib/active_record/nested_attributes.rb#L1) class. 67 | 68 | ### View 69 | 70 | The next step is set up the form view with the `nested_fields_for` method. It receives the association/collection name, an optional hash of options (humm, a pun) and a block with the nested fields. Proceeding with the person/phones example, we can have a form like this: 71 | 72 | ```erb 73 | <%= form_for(@person) do |f| %> 74 | <% # person fields... %> 75 | 76 |
There are no phones.
131 | <% end %> 132 | <% end %> 133 | ``` 134 | 135 | And yeah, you need to mark it with the class `empty` or any other selector configured via javascript. 136 | 137 | #### render_template 138 | 139 | When `nested_fields_for` is called, it also includes a `