diff --git a/lib/nested_form/builder_mixin.rb b/lib/nested_form/builder_mixin.rb index b572cf07..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,6 +33,24 @@ def link_to_add(*args, &block) options[:class] = [options[:class], "add_nested_fields"].compact.join(" ") options["data-association"] = association + 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 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 dcdbc8ac..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,6 +43,8 @@ content = $.trim(content.replace(regexp, new_id)); var field = this.insertFields(content, assoc, link); + checkMaximum(); + // 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(); + + checkMaximum(); field .trigger({ type: 'nested:fieldRemoved', field: field }) @@ -81,6 +85,21 @@ $(document) .delegate('form a.add_nested_fields', 'click', nestedFormEvents.addFields) .delegate('form a.remove_nested_fields', 'click', nestedFormEvents.removeFields); + + // if maximum is set for this nested + function checkMaximum() { + $('form a.add_nested_fields').each(function(){ + 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(); + } + }); + } + + 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); +})();