3

I'm using the jQuery Validation plugin on my forms. I have some groups of fields that are optional, but need to be either "all or nothing" - if you fill one input in a group, you must fill all of them.

For example, imagine the user can input one location - street, city, state, and zip - or multiple locations. You don't have to enter a second location, but if you do, it's not OK to just give the city; I need the state and zip for that one too.

To solve this problem, I wrote a custom rule - really just a tiny tweak of my previous rule, require_from_group. This one is called skip_or_fill_minimum. Here's how you'd use it:

var validationrules = {
  rules: {
    location2address: {
    skip_or_fill_minimum: [4,'.location2']
  }
  //This input will validate if all 4 `.location2` inputs are filled,
  //or if all of them are left blank
}

var validator = $("#formtovalidate").validate(validationrules);  

Here's the code for the custom rule:

jQuery.validator.addMethod("skip_or_fill_minimum", function(value, element, options) {
    var numberRequired = options[0];
    var selector = options[1];
    //Look for our selector within the parent form
    var numberFilled = $(selector, element.form).filter(function() {
         // Each field is kept if it has a value
         return $(this).val();
      }).length;
      var valid = numberFilled >= numberRequired || numberFilled === 0;

    //The elegent part - this element needs to check the others that match the
    //selector, but we don't want to set off a feedback loop where all the
    //elements check all the others which check all the others which
    //check all the others...
    //So instead we
    //  1) Flag all matching elements as 'currently being validated'
    //  using jQuery's .data()
    //  2) Re-run validation on each of them. Since the others are now
    //     flagged as being in the process, they will skip this section,
    //     and therefore won't turn around and validate everything else
    //  3) Once that's done, we remove the 'currently being validated' flag
    //     from all the elements
    if(!$(element).data('being_validated')) {
      var fields = $(selector, element.form);
      //.valid() means "validate using all applicable rules" (which includes this one)
      fields.data('being_validated', true);
      fields.validate(); //Changed from fields.valid();
      fields.data('being_validated', false);
    }
    return valid;
    // {0} below is the 0th item in the options field
    }, jQuery.format("Please either skip these fields or fill at least {0} of them."));

I'm primarily posting this so that others who search the web for a solution will find it. Still - does anyone see a way to improve this?

Update - now part of jQuery Validation

This was officially added to jQuery Validation on 4/3/2012.

Community
  • 1
  • 1
Nathan Long
  • 122,748
  • 97
  • 336
  • 451
  • Updated 4-9-2010 with the elegant part that re-checks other group members without causing an infinite loop. That logic is courtesy of Nick Craver - see this question about my related rule: http://stackoverflow.com/questions/1378472/jquery-validate-can-i-re-validate-a-group-of-fields-after-changing-one – Nathan Long Apr 09 '10 at 18:58
  • Updated 8-22-2010 - changed .valid() to .validate() on fields.data; valid() was causing the whole form to pass validation when just these fields returned true. – Nathan Long Aug 22 '10 at 18:39

4 Answers4

4

My shortened and "optimized" version (more chaining + use of validator custom provided selector). And hopefully makes less selection operations.

Still suffers from the same bugs your rule does -- what I mean is that the removing of the error class and the error labels on all elems which match our selector is not really correct. Just imagine you require 4 fields to be set but there are 5. User fills 4 fields. The fifth he leaves unfilled but on that field you additionally set required and email. This way the error-message for the email too gets removed although he didn't resolve it yet.

jQuery.validator.addMethod("skip_or_fill_minimum", function(value, element, options) {
    var elems = $(element).parents('form').find(options[1]);
    var numberFilled = elems.filter(':filled').size();
    if (numberFilled >= options[0] || numberFilled == 0) {
        elems.removeClass('error');
        elems.next('label.error').not('.checked').text('').addClass('checked');
        return true;
    } else {
        elems.addClass('error');
    }
}, jQuery.format("Please either skip these fields or fill at least {0} of them."));
jitter
  • 53,475
  • 11
  • 111
  • 124
  • Good suggestions. To some extent, my code was verbose on purpose in order to be a clear example, but I can learn a couple of things from your code. Thank you! Also - you're right about the incorrect behavior removing the errors. Any ideas for a workaround? – Nathan Long Dec 12 '09 at 09:57
1

It took me quite a while to find the fix mentioned above for the problem where fields above the group would not get validated.

It would be good if the change of .valid() to .validate() was added to the closely related questions - here and here as many people are asking about it.

This fix though stops all the groups labels from being shown or removed on key or blur events. I found jitter's if else statement above to be very helpful in solving that.

note: stackoverflow has help me find many answers in the past, but this is my first actual contribution. Thanks all for bring together this solution.

Community
  • 1
  • 1
1

This is my first post to SO, so I hope I'm doing this correctly, but there appears to be an error in the accepted solution. I have added the closing bracket for the location2address block (or the rules block, depending on how you look at it).

var validationrules = {
  rules: {
    location2address: {
      skip_or_fill_minimum: [4,'.location2']
    }
  }
  //This input will validate if all 4 `.location2` inputs are filled,
  //or if all of them are left blank
}
var validator = $("#groovetime_entry").validate(validationrules); 
bergie3000
  • 1,091
  • 1
  • 13
  • 21
0

I needed something slightly different. It is a search form with Either or Both first or last name AND/OR 1 of 4 other search criteria.

I wanted them to either skip or ONLY enter one (or x). I modified the code above:

    jQuery.validator.addMethod("skip_or_fill_only_x", function (value, element, options) {
    var numberRequired = options[0];
    var selector = options[1];
    var numberFilled = $(selector, element.form).filter(function () {
        return $(this).val();
    }).length;
    //Changed this line from "numberFilled >=" to "numberFilled ==="
    var valid = numberFilled === numberRequired || numberFilled === 0;
    if (!$(element).data('being_validated')) {
        var fields = $(selector, element.form);
        fields.data('being_validated', true);
        fields.validate();
        fields.data('being_validated', false);
    }
    return valid;
//Adjusted message
}, jQuery.format("Please either skip these fields or only fill {0} of them."));

the full implementation looks like this:

    $('#advancedSearchForm').validate({
    errorLabelContainer: '#advancedErrors',
    rules: {
        FirstName: {
            require_from_group: [1, '.mygroup']
        },
        LastName: {
            require_from_group: [1, '.mygroup']
        },
        countryCodeAdvanced: {
            skip_or_fill_only_x: [1, '.location2']
        },
        location: {
            skip_or_fill_only_x: [1, '.location2']
        },
        PositionTitle: {
            skip_or_fill_only_x: [1, '.location2']
        },
        FunctionalArea: {
            skip_or_fill_only_x: [1, '.location2']
        }
    },
    groups: {
        mygroup: "FirstName LastName",
        location2: "countryCodeAdvanced location PositionTitle FunctionalArea"
    }
});


jQuery.validator.addMethod("skip_or_fill_only_x", function (value, element, options) {
    var numberRequired = options[0];
    var selector = options[1];
    var numberFilled = $(selector, element.form).filter(function () {
        return $(this).val();
    }).length;
    var valid = numberFilled === numberRequired || numberFilled === 0;
    if (!$(element).data('being_validated')) {
        var fields = $(selector, element.form);
        fields.data('being_validated', true);
        fields.validate();
        fields.data('being_validated', false);
    }
    return valid;
}, jQuery.format("Please either skip these fields or only fill {0} of them."));