From c0b943021733c109ac25a8e09336fff1e66647d3 Mon Sep 17 00:00:00 2001 From: Sebastian Edwards Date: Fri, 27 May 2011 11:43:37 +1200 Subject: [PATCH 1/2] Hides the add link when object reaches maximum number of nested objects as defined in length validation. --- lib/nested_form/builder_mixin.rb | 9 +++++++++ vendor/assets/javascripts/jquery_nested_form.js | 14 ++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/lib/nested_form/builder_mixin.rb b/lib/nested_form/builder_mixin.rb index b572cf07..da5fc4ea 100644 --- a/lib/nested_form/builder_mixin.rb +++ b/lib/nested_form/builder_mixin.rb @@ -31,6 +31,14 @@ def link_to_add(*args, &block) options[:class] = [options[:class], "add_nested_fields"].compact.join(" ") options["data-association"] = association + validators = self.object.class.validators_on(association) + unless validators.empty? + length_validator = validators.select {|item| item.class == ActiveModel::Validations::LengthValidator}.first + unless length_validator.nil? + maximum = length_validator.options[:maximum] + options["data-maximum"] = maximum + end + end options["data-blueprint-id"] = fields_blueprint_id = fields_blueprint_id_for(association) args << (options.delete(:href) || "javascript:void(0)") args << options @@ -89,6 +97,7 @@ def fields_for_with_nested_attributes(association_name, *args) def fields_for_nested_model(name, object, options, block) classes = 'fields' + classes << " #{object.class.name.underscore.pluralize}" classes << ' marked_for_destruction' if object.respond_to?(:marked_for_destruction?) && object.marked_for_destruction? perform_wrap = options.fetch(:nested_wrapper, true) diff --git a/vendor/assets/javascripts/jquery_nested_form.js b/vendor/assets/javascripts/jquery_nested_form.js index dcdbc8ac..c9f8a154 100644 --- a/vendor/assets/javascripts/jquery_nested_form.js +++ b/vendor/assets/javascripts/jquery_nested_form.js @@ -43,6 +43,8 @@ content = $.trim(content.replace(regexp, new_id)); var field = this.insertFields(content, assoc, link); + check_maximum(); + // bubble up event upto document (through form) field .trigger({ type: 'nested:fieldAdded', field: field }) @@ -69,6 +71,8 @@ var field = $link.closest('.fields'); field.hide(); + + check_maximum(); field .trigger({ type: 'nested:fieldRemoved', field: field }) @@ -81,6 +85,16 @@ $(document) .delegate('form a.add_nested_fields', 'click', nestedFormEvents.addFields) .delegate('form a.remove_nested_fields', 'click', nestedFormEvents.removeFields); + + function check_maximum() { + $('form a.add_nested_fields').each(function(){ + var assoc = $(this).attr('data-association'); // Name of child + var maximum = $(this).attr('data-maximum'); // Maximum # of children + $('.' + assoc+':visible').length >= maximum ? $(this).hide() : $(this).show(); + }); + } + + check_maximum(); })(jQuery); // http://plugins.jquery.com/project/closestChild From 63ed592c623fb22471eb78b28a2cd4018fb9f51a Mon Sep 17 00:00:00 2001 From: Michael Glass Date: Tue, 19 Feb 2013 10:21:46 -0800 Subject: [PATCH 2/2] migrated length validators forward, added prototype. --- lib/nested_form/builder_mixin.rb | 23 ++- lib/nested_form/engine.rb | 7 + .../unresolved_length_validator.rb | 4 + .../assets/javascripts/jquery_nested_form.js | 21 ++- .../javascripts/prototype_nested_form.js | 139 +++++++++++------- 5 files changed, 124 insertions(+), 70 deletions(-) create mode 100644 lib/nested_form/unresolved_length_validator.rb diff --git a/lib/nested_form/builder_mixin.rb b/lib/nested_form/builder_mixin.rb index da5fc4ea..98331c7c 100644 --- a/lib/nested_form/builder_mixin.rb +++ b/lib/nested_form/builder_mixin.rb @@ -1,3 +1,5 @@ +require 'nested_form/unresolved_length_validator' + module NestedForm module BuilderMixin # 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. @@ -31,14 +33,24 @@ def link_to_add(*args, &block) options[:class] = [options[:class], "add_nested_fields"].compact.join(" ") options["data-association"] = association - validators = self.object.class.validators_on(association) - unless validators.empty? - length_validator = validators.select {|item| item.class == ActiveModel::Validations::LengthValidator}.first - unless length_validator.nil? - maximum = length_validator.options[:maximum] + if Rails.application.config.nested_form.use_length_validators || options[:check_maximum] || options[:maximum] + unless maximum = options.delete(:maximum) + validators = self.object.class.validators_on(association) + length_validators = validators.select {|item| item.is_a? ActiveModel::Validations::LengthValidator} + if length_validators.length == 1 + maximum = length_validators[0].options[:maximum] + end + end + + force_check_maximum = options.delete :check_maximum + + if maximum options["data-maximum"] = maximum + elsif force_check_maximum + raise UnresolvedLengthValidator, "nested form builder could not find length of #{association}" end end + options["data-blueprint-id"] = fields_blueprint_id = fields_blueprint_id_for(association) args << (options.delete(:href) || "javascript:void(0)") args << options @@ -97,7 +109,6 @@ def fields_for_with_nested_attributes(association_name, *args) def fields_for_nested_model(name, object, options, block) classes = 'fields' - classes << " #{object.class.name.underscore.pluralize}" classes << ' marked_for_destruction' if object.respond_to?(:marked_for_destruction?) && object.marked_for_destruction? perform_wrap = options.fetch(:nested_wrapper, true) diff --git a/lib/nested_form/engine.rb b/lib/nested_form/engine.rb index ba14a1f1..48603fd8 100644 --- a/lib/nested_form/engine.rb +++ b/lib/nested_form/engine.rb @@ -11,4 +11,11 @@ class ActionView::Base end end end + + class Railtie < Rails::Railtie + config.nested_form = ActiveSupport::OrderedOptions.new + #set to true for global use of length validators to hide and show + #add and remove buttons + config.nested_form.use_length_validators = false + end end diff --git a/lib/nested_form/unresolved_length_validator.rb b/lib/nested_form/unresolved_length_validator.rb new file mode 100644 index 00000000..4e372d3e --- /dev/null +++ b/lib/nested_form/unresolved_length_validator.rb @@ -0,0 +1,4 @@ +module NestedForm + class UnresolvedLengthValidatorError < StandardError + end +end diff --git a/vendor/assets/javascripts/jquery_nested_form.js b/vendor/assets/javascripts/jquery_nested_form.js index c9f8a154..5b63c757 100644 --- a/vendor/assets/javascripts/jquery_nested_form.js +++ b/vendor/assets/javascripts/jquery_nested_form.js @@ -14,7 +14,7 @@ // Make the context correct by replacing with the generated ID // of each of the parent objects - var context = ($(link).closest('.fields').closestChild('input, textarea, select').eq(0).attr('name') || '').replace(new RegExp('\[[a-z_]+\]$'), ''); + var context = ($(link).closest('.fields').closestChild('[name]').eq(0).attr('name') || '').replace(new RegExp('\[[a-z_]+\]$'), ''); // context will be something like this for a brand new form: // project[tasks_attributes][1255929127459][assignments_attributes][1255929128105] @@ -43,7 +43,7 @@ content = $.trim(content.replace(regexp, new_id)); var field = this.insertFields(content, assoc, link); - check_maximum(); + checkMaximum(); // bubble up event upto document (through form) field @@ -72,7 +72,7 @@ var field = $link.closest('.fields'); field.hide(); - check_maximum(); + checkMaximum(); field .trigger({ type: 'nested:fieldRemoved', field: field }) @@ -86,15 +86,20 @@ .delegate('form a.add_nested_fields', 'click', nestedFormEvents.addFields) .delegate('form a.remove_nested_fields', 'click', nestedFormEvents.removeFields); - function check_maximum() { + // if maximum is set for this nested + function checkMaximum() { $('form a.add_nested_fields').each(function(){ - var assoc = $(this).attr('data-association'); // Name of child - var maximum = $(this).attr('data-maximum'); // Maximum # of children - $('.' + assoc+':visible').length >= maximum ? $(this).hide() : $(this).show(); + var maximum = $(this).data('maximum'); // Maximum # of children + if(maximum != null) { + var assoc = $(this).data('association'); // Name of child + var fields_selector = "div.fields :has(input[name*='["+ assoc +"_attributes]']):visible"; + fields = $(this).siblings(fields_selector) + fields.length >= maximum ? $(this).hide() : $(this).show(); + } }); } - check_maximum(); + checkMaximum(); })(jQuery); // http://plugins.jquery.com/project/closestChild diff --git a/vendor/assets/javascripts/prototype_nested_form.js b/vendor/assets/javascripts/prototype_nested_form.js index 8a63e38d..e520f717 100644 --- a/vendor/assets/javascripts/prototype_nested_form.js +++ b/vendor/assets/javascripts/prototype_nested_form.js @@ -1,63 +1,90 @@ -document.observe('click', function(e, el) { - if (el = e.findElement('form a.add_nested_fields')) { - // Setup - var assoc = el.readAttribute('data-association'); // Name of child - var target = el.readAttribute('data-target'); - var blueprint = $(el.readAttribute('data-blueprint-id')); - var content = blueprint.readAttribute('data-blueprint'); // Fields template - - // Make the context correct by replacing with the generated ID - // of each of the parent objects - var context = (el.getOffsetParent('.fields').firstDescendant().readAttribute('name') || '').replace(new RegExp('\[[a-z_]+\]$'), ''); - - // context will be something like this for a brand new form: - // project[tasks_attributes][1255929127459][assignments_attributes][1255929128105] - // or for an edit form: - // project[tasks_attributes][0][assignments_attributes][1] - if(context) { - var parent_names = context.match(/[a-z_]+_attributes(?=\]\[(new_)?\d+\])/g) || []; - var parent_ids = context.match(/[0-9]+/g) || []; - - for(i = 0; i < parent_names.length; i++) { - if(parent_ids[i]) { - content = content.replace( - new RegExp('(_' + parent_names[i] + ')_.+?_', 'g'), - '$1_' + parent_ids[i] + '_'); - - content = content.replace( - new RegExp('(\\[' + parent_names[i] + '\\])\\[.+?\\]', 'g'), - '$1[' + parent_ids[i] + ']'); +(function() { + //equivalent to addFields + document.observe('click', function(e, el) { + if (el = e.findElement('form a.add_nested_fields')) { + // Setup + var assoc = el.readAttribute('data-association'); // Name of child + var target = el.readAttribute('data-target'); + var blueprint = $(el.readAttribute('data-blueprint-id')); + var content = blueprint.readAttribute('data-blueprint'); // Fields template + + // Make the context correct by replacing with the generated ID + // of each of the parent objects + var context = (el.getOffsetParent('.fields').firstDescendant().readAttribute('name') || '').replace(new RegExp('\[[a-z_]+\]$'), ''); + + // context will be something like this for a brand new form: + // project[tasks_attributes][1255929127459][assignments_attributes][1255929128105] + // or for an edit form: + // project[tasks_attributes][0][assignments_attributes][1] + if(context) { + var parent_names = context.match(/[a-z_]+_attributes(?=\]\[(new_)?\d+\])/g) || []; + var parent_ids = context.match(/[0-9]+/g) || []; + + for(i = 0; i < parent_names.length; i++) { + if(parent_ids[i]) { + content = content.replace( + new RegExp('(_' + parent_names[i] + ')_.+?_', 'g'), + '$1_' + parent_ids[i] + '_'); + + content = content.replace( + new RegExp('(\\[' + parent_names[i] + '\\])\\[.+?\\]', 'g'), + '$1[' + parent_ids[i] + ']'); + } } } - } - // Make a unique ID for the new child - var regexp = new RegExp('new_' + assoc, 'g'); - var new_id = new Date().getTime(); - content = content.replace(regexp, new_id); + // Make a unique ID for the new child + var regexp = new RegExp('new_' + assoc, 'g'); + var new_id = new Date().getTime(); + content = content.replace(regexp, new_id); - var field; - if (target) { - field = $$(target)[0].insert(content); - } else { - field = el.insert({ before: content }); + checkMaximum(); + + var field; + if (target) { + field = $$(target)[0].insert(content); + } else { + field = el.insert({ before: content }); + } + field.fire('nested:fieldAdded', {field: field}); + field.fire('nested:fieldAdded:' + assoc, {field: field}); + return false; } - field.fire('nested:fieldAdded', {field: field}); - field.fire('nested:fieldAdded:' + assoc, {field: field}); - return false; - } -}); - -document.observe('click', function(e, el) { - if (el = e.findElement('form a.remove_nested_fields')) { - var hidden_field = el.previous(0), - assoc = el.readAttribute('data-association'); // Name of child to be removed - if(hidden_field) { - hidden_field.value = '1'; + }); + + //equivalent to removeFields + document.observe('click', function(e, el) { + if (el = e.findElement('form a.remove_nested_fields')) { + var hidden_field = el.previous(0), + assoc = el.readAttribute('data-association'); // Name of child to be removed + if(hidden_field) { + hidden_field.value = '1'; + } + + checkMaximum(); + + var field = el.up('.fields').hide(); + field.fire('nested:fieldRemoved', {field: field}); + field.fire('nested:fieldRemoved:' + assoc, {field: field}); + return false; } - var field = el.up('.fields').hide(); - field.fire('nested:fieldRemoved', {field: field}); - field.fire('nested:fieldRemoved:' + assoc, {field: field}); - return false; + }); + + function checkMaximum() { + var add_buttons = $$('form a.add_nested_fields'); + for(var i = 0; i < add_buttons.length; i++) { + var el = add_buttons[i]; + var maximum = el.readAttribute('data-maximum'); // Maximum # of children + if(maximum != null) { + var assoc = el.readAttribute('data-association'); + var fields_selector = "div.fields :has(input[name*='["+ assoc +"_attributes]']):visible"; + var fields = el.siblings().filter(function(sibling) { + return sibling.match(fields_selector); + }); + fields.length >= maximum ? el.hide() : el.show(); + } + }; } -}); + + document.observe('dom:loaded', checkMaximum); +})();