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

JqueryValidationJquery PluginsJquery Validate

Jquery Problem Overview


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.

Jquery Solutions


Solution 1 - Jquery

That's an excellent solution Nathan. Thanks a lot.

Here's a way making the above code work, in case someone runs into trouble integrating it, like I did:

Code inside the additional-methods.js file:

jQuery.validator.addMethod("require_from_group", function(value, element, options) {
...// Nathan's code without any changes
}, jQuery.format("Please fill out at least {0} of these fields."));

// "filone" is the class we will use for the input elements at this example
jQuery.validator.addClassRules("fillone", {
    require_from_group: [1,".fillone"]
});

Code inside the html file:

<input id="field1" class="fillone" type="text" value="" name="field1" />
<input id="field2" class="fillone" type="text" value="" name="field2" />
<input id="field3" class="fillone" type="text" value="" name="field3" />
<input id="field4" class="fillone" type="text" value="" name="field4" />

Don't forget to include additional-methods.js file!

Solution 2 - Jquery

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 3 - Jquery

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 4 - Jquery

I've submitted a [patch][1] 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][2].

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."));

[1]: https://github.com/kshade2001/jquery-validation/blob/38c2b3223b43017f8fc196ee16461a147b7f94a6/src/additional/require_from_group.js "patch" [2]: http://github.com/jzaefferer/jquery-validation/issues/412 "github"

Solution 5 - Jquery

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 6 - Jquery

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.

Solution 7 - Jquery

Because the form I'm working on has several cloned regions with grouped inputs like these, I passed an extra argument to the require_from_group constructor, changing exactly one line of your addMethod function:

var commonParent = $(element).parents(options[2]);

and this way a selector, ID or element name can be passed once:

jQuery.validator.addClassRules("reqgrp", {require_from_group: [1, ".reqgrp", 'fieldset']});

and the validator will restrict the validation to elements with that class only inside each fieldset, rather than try to count all the .reqgrp classed elements in the form.

Solution 8 - Jquery

I was having problems with other rules not being checked in conjunction with this, so I changed:

fields.valid();

To this:

var validator = this;
fields.each(function(){
   validator.valid(this);
});

I also made a few (personal) improvements, and this is the version I'm using:

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);
	}
	return validOrNot;
}, jQuery.format("Please fill out at least {0} of these fields."));

Solution 9 - Jquery

Thanks, Nathan. You saved me a ton of time.

However, i must notice that this rule isn't jQuery.noConflict() ready. So, one must replace all $ with jQuery to work with, say, var $j = jQuery.noConflict()

And i have question: how would i make it behave like built-in rule? For example, if i enter email, the message "Please enter valid email" disappears automatically but if i fill one of group fields error message stays.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionNathan LongView Question on Stackoverflow
Solution 1 - JqueryGerasimosView Answer on Stackoverflow
Solution 2 - JqueryseanView Answer on Stackoverflow
Solution 3 - JqueryWalter KellyView Answer on Stackoverflow
Solution 4 - JqueryK MView Answer on Stackoverflow
Solution 5 - JqueryMichael GundlachView Answer on Stackoverflow
Solution 6 - JquerysquarecandyView Answer on Stackoverflow
Solution 7 - JqueryAndrew RoazenView Answer on Stackoverflow
Solution 8 - Jquerygen_EricView Answer on Stackoverflow
Solution 9 - JqueryRinatView Answer on Stackoverflow