├── 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 [![Gem Version](https://badge.fury.io/rb/awesome_nested_fields.png)](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 |

Phones

77 |
78 | <%= f.nested_fields_for :phones do |f| %> 79 |
80 | <%= f.label :number %> 81 | <%= f.text_field :number %> 82 | 83 | remove 84 | 85 | <%= f.hidden_field :id %> 86 | <%= f.hidden_field :_destroy %> 87 |
88 | <% end %> 89 |
90 | add phone 91 | 92 | <% # more person fields... %> 93 | <% end %> 94 | ``` 95 | 96 | The `nested_fields_for` method lists the phones this person has and also adds an empty template to the page for creating new phones. (Actually, there is too much code inside the block. If you're not working with a simple example like this you better extract this code into a partial and call just `render :phones` inside the block. Good coding practices, you know.) 97 | 98 | If you're paying attention, you noticed the key elements are marked with special class names. We *need* this for the javascript code, so it knows what to do with each HTML element: the one that have the children must have the class `items`; each child must be marked with the class `item`; inside an item, the link for removal must have the class `remove`; and the link to add new items must have the class `add`. We can change the names later, but these are the default choices. Finally, don't forget to add the `id` field, as it is needed by AR to identify whether this is an existing or a new element, and the `_destroy` field to activate deletion when the user clicks on the remove link. 99 | 100 | ### Javascript 101 | 102 | This is the easiest part: just activate the nested fields actions when the page loads. We can put this in the `application.js` file (or in any other place that gets executed in the page): 103 | 104 | ```javascript 105 | $(document).ready(function(e) { 106 | $('FORM').nestedFields(); 107 | }); 108 | ``` 109 | 110 | Now enjoy your new nested model form! 111 | 112 | 113 | Reference 114 | --------- 115 | 116 | ### View Options 117 | 118 | There are some view options, but most are internal. There is just one you really need to know about; for the others, go to the code. 119 | 120 | #### show_empty 121 | 122 | Sometimes you want to show something when the collection is empty. Just set `show_empty` to `true` and prepare the block to receive `nil` when the collection is empty. Awesome nested fields will take care to show the empty message when there are no elements and remove it when one is added. 123 | To implement this on the basic example, do something like: 124 | 125 | ```erb 126 | <%= f.nested_fields_for :phones, show_empty: true do |f| %> 127 | <% if f %> 128 | <% fields code... %> 129 | <% else %> 130 |

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 `