jQuery Validate - require at least one field in a group to be filled

73,164

Solution 1

Nice solution. However, I had the problem of other required rules not working. Executing .valid() against the form fixed this issue for me.

if(!$(element).data('being_validated')) {
  var fields = $(selector, element.form);
  fields.data('being_validated', true); 
  $(element.form).valid();
  fields.data('being_validated', false);
}

Solution 2

Thanks sean. That fixed the issue I had with the code ignoring other rules.

I also made a few changes so that 'Please fill out at least 1 field ..' message shows in a separate div instead of after all every field.

put in form validate script

showErrors: function(errorMap, errorList){
            $("#form_error").html("Please fill out at least 1 field before submitting.");
            this.defaultShowErrors();
        },

add this somewhere in the page

<div class="error" id="form_error"></div>

add to the require_from_group method addMethod function

 if(validOrNot){
    $("#form_error").hide();
}else{
    $("#form_error").show();
}
......
}, jQuery.format(" &nbsp;(!)"));

Solution 3

I've submitted a patch that doesn't suffer from the issues the current version does (whereby the "required" option stops working properly on other fields, a discussion of the problems with the current version is on github.

Example at http://jsfiddle.net/f887W/10/

jQuery.validator.addMethod("require_from_group", function (value, element, options) {
var validator = this;
var minRequired = options[0];
var selector = options[1];
var validOrNot = jQuery(selector, element.form).filter(function () {
    return validator.elementValue(this);
}).length >= minRequired;

// remove all events in namespace require_from_group
jQuery(selector, element.form).off('.require_from_group');

//add the required events to trigger revalidation if setting is enabled in the validator
if (this.settings.onkeyup) {
    jQuery(selector, element.form).on({
        'keyup.require_from_group': function (e) {
            jQuery(selector, element.form).valid();
        }
    });
}

if (this.settings.onfocusin) {
    jQuery(selector, element.form).on({
        'focusin.require_from_group': function (e) {
            jQuery(selector, element.form).valid();
        }
    });
}

if (this.settings.click) {
    jQuery(selector, element.form).on({
        'click.require_from_group': function (e) {
            jQuery(selector, element.form).valid();
        }
    });
}

if (this.settings.onfocusout) {
    jQuery(selector, element.form).on({
        'focusout.require_from_group': function (e) {
            jQuery(selector, element.form).valid();
        }
    });
}

return validOrNot;
}, jQuery.format("Please fill at least {0} of these fields."));

Solution 4

Starting a variable name with $ is required in PHP, but pretty weird (IMHO) in Javascript. Also, I believe you refer to it as "$module" twice and "module" once, right? It seems that this code shouldn't work.

Also, I'm not sure if it's normal jQuery plugin syntax, but I might add comments above your addMethod call, explaining what you accomplish. Even with your text description above, it's hard to follow the code, because I'm not familiar with what fieldset, :filled, value, element, or selector refer to. Perhaps most of this is obvious to someone familiar with the Validate plugin, so use judgment about what is the right amount of explanation.

Perhaps you could break out a few vars to self-document the code; like,

var atLeastOneFilled = module.find(...).length > 0;
if (atLeastOneFilled) {
  var stillMarkedWithErrors = module.find(...).next(...).not(...);
  stillMarkedWithErrors.text("").addClass(...)

(assuming I did understand the meaning of these chunks of your code! :) )

I'm not exactly sure what "module" means, actually -- is there a more specific name you could give to this variable?

Nice code, overall!

Solution 5

Here's my crack at Rocket Hazmat's answer, trying to solve the issue of other defined fields also needing to be validated, but marking all fields as valid on successful filling of one.

jQuery.validator.addMethod("require_from_group", function(value, element, options){
    var numberRequired = options[0],
    selector = options[1],
    $fields = $(selector, element.form),
    validOrNot = $fields.filter(function() {
        return $(this).val();
    }).length >= numberRequired,
    validator = this;
    if(!$(element).data('being_validated')) {
        $fields.data('being_validated', true).each(function(){
            validator.valid(this);
        }).data('being_validated', false);
    }
    if (validOrNot) {
    $(selector).each(function() {
            $(this).removeClass('error');
            $('label.error[for='+$(this).attr('id')+']').remove();
        });
    }
    return validOrNot;
}, jQuery.format("Please fill out at least {0} of these fields."));

The only remaining issue with this now is the edge case where the field is empty, then filled, then empty again... in which case the error will by applied to the single field not the group. But that seems so unlikely to happen with any frequency and it still technically works in that case.

Share:
73,164
Nathan Long
Author by

Nathan Long

I code mostly in Elixir, sometimes Ruby. More about me at nathanmlong.com and Stackoverflow Careers.

Updated on July 12, 2020

Comments

  • Nathan Long
    Nathan Long almost 4 years

    I'm using the excellent jQuery Validate Plugin to validate some forms. On one form, I need to ensure that the user fills in at least one of a group of fields. I think I've got a pretty good solution, and wanted to share it. Please suggest any improvements you can think of.

    Finding no built-in way to do this, I searched and found Rebecca Murphey's custom validation method, which was very helpful.

    I improved this in three ways:

    1. To let you pass in a selector for the group of fields
    2. To let you specify how many of that group must be filled for validation to pass
    3. To show all inputs in the group as passing validation as soon as one of them passes validation. (See shout-out to Nick Craver at end.)

    So you can say "at least X inputs that match selector Y must be filled."

    The end result, with markup like this:

    <input class="productinfo" name="partnumber">
    <input class="productinfo" name="description">
    

    ...is a group of rules like this:

    // Both these inputs input will validate if 
    // at least 1 input with class 'productinfo' is filled
    partnumber: {
       require_from_group: [1,".productinfo"]
      }
    description: {
       require_from_group: [1,".productinfo"]
    }
    

    Item #3 assumes that you're adding a class of .checked to your error messages upon successful validation. You can do this as follows, as demonstrated here.

    success: function(label) {  
            label.html(" ").addClass("checked"); 
    }
    

    As in the demo linked above, I use CSS to give each span.error an X image as its background, unless it has the class .checked, in which case it gets a check mark image.

    Here's my code so far:

    jQuery.validator.addMethod("require_from_group", function(value, element, options) {
        var numberRequired = options[0];
        var selector = options[1];
        //Look for our selector within the parent form
        var validOrNot = $(selector, element.form).filter(function() {
             // Each field is kept if it has a value
             return $(this).val();
             // Set to true if there are enough, else to false
          }).length >= numberRequired;
    
        // 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 each element
        // has to check each other element. It would be like:
        // Element 1: "I might be valid if you're valid. Are you?"
        // Element 2: "Let's see. I might be valid if YOU'RE valid. Are you?"
        // Element 1: "Let's see. I might be valid if YOU'RE valid. Are you?"
        // ...etc, until we get a "too much recursion" error.
        //
        // 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);
        fields.data('being_validated', true);
        // .valid() means "validate using all applicable rules" (which 
        // includes this one)
        fields.valid();
        fields.data('being_validated', false);
        }
        return validOrNot;
        // {0} below is the 0th item in the options field
        }, jQuery.format("Please fill out at least {0} of these fields."));
    

    Hooray!

    Shout out

    Now for that shout-out - originally, my code just blindly hid the error messages on the other matching fields instead of re-validating them, which meant that if there was another problem (like 'only numbers are allowed and you entered letters'), it got hidden until the user tried to submit. This was because I didn't know how to avoid the feedback loop mentioned in the comments above. I knew there must be a way, so I asked a question, and Nick Craver enlightened me. Thanks, Nick!

    Question Solved

    This was originally a "let me share this and see if anybody can suggest improvements" kind of question. While I'd still welcome feedback, I think it's pretty complete at this point. (It could be shorter, but I want it to be easy to read and not necessarily concise.) So just enjoy!

    Update - now part of jQuery Validation

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