98

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.

Community
  • 1
  • 1
Nathan Long
  • 122,748
  • 97
  • 336
  • 451
  • Also, see closely related rule - “Either skip these fields, or fill at least X of them" - http://stackoverflow.com/questions/1888976/jquery-validate-either-skip-these-fields-or-fill-at-least-x-of-them – Nathan Long Apr 09 '10 at 19:10
  • Why would one arbitrary input be responsible for checking if other inputs are filled? This doesn't make sense. Perhaps you could include a bit of markup with the elements involved? – montrealist May 30 '11 at 15:10
  • @dalbaeb - I clarified the example a bit. It's not that one arbitrary input is responsible for checking others; it's that each input in a group is responsible for checking all the others. – Nathan Long May 31 '11 at 12:29
  • That's what I thought, thanks very much! – montrealist Jun 08 '11 at 17:33
  • 3
    Thanks, this works for me, but the other required fields in the form now do not respond anymore unless they gain and lose focus subsequent to the check. (Someone added this as an answer on your other question, but it had to be flagged because it's not an answer). – mydoghasworms Sep 05 '11 at 07:15
  • Specifically, fields appearing before the group in a form suffer this problem, while fields appearing after seem fine. The user Marcus (550499) had an example here: http://jsfiddle.net/mhmBs/132/ – mydoghasworms Sep 05 '11 at 07:31
  • I ran into some real weirdness when I used errorPlacement (the error list I was adding to grew spontaneously); after removing that, everything worked great. – mooreds Oct 06 '11 at 22:27
  • cool script but what if i were to use 2 calling to the function....i couldnt get it validate properly... $.validator.addClassRules("filloneQualifier", { require_from_group: [1,".filloneQualifier"] }); $.validator.addClassRules("filloneReward", { require_from_group: [1,".filloneReward"] }); – lilsizzo Nov 17 '11 at 09:50
  • what if the field has a name such ast prices[]...what should i be putting in the group naming there? – lilsizzo Nov 18 '11 at 07:56
  • How would this work if you're setting default field values as the labels for those fields? (e.g. the search field at the top of Stack Overflow.) These fields don't default to null, so you need to be able to specify what specific values should also constitute a null state. – Jeremy Jan 21 '12 at 20:57
  • Thanks for this! I was trying the same thing but didn't get item #3 right. Here's a shorter version of the validOrNot check: `var validOrNot = $(selector + ':filled', element.form).length >= numberRequired;` – Christof Mar 06 '12 at 13:07

9 Answers9

21

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!

  • Glad it's helpful to you, and thanks for chipping in information. However, instead of doing the addClassRules method, I prefer to use an array of rules on each individual form. If you go to this page (http://jquery.bassistance.de/validate/demo/milk/) and click "show script used on this page" you will see an example. I take it one step further: I declare an array called "rules", then separately, I use them with var validator = $('#formtovalidate').validate(rules); – Nathan Long Sep 28 '09 at 11:42
  • Another thought: the 'fillone' class you show here could be problematic. What if, on the same form, you need to require at least one part number, AND at least one contact name? Your rule will allow 0 contact names as long as there's at least one part number. I think it's better to set rules like `require_from_group: [1,".partnumber"]` and `...[1,".contactname"]` to ensure you're validating the right things. – Nathan Long Dec 08 '09 at 17:39
6

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);
}
sean
  • 11,164
  • 8
  • 48
  • 56
  • 1
    Thanks Sean, I was having this problem too. There's one problem with this solution though, when user gets to the form for the first time - as soon as he fills out the first require-from-group field, all other form fields will be validated and thus marked as faulty. The way I solved this was to add a form.submit() handler before creating an instance of the validator, in which I set a flag `validator.formSubmit = true`. In the require-from-group method I then check for that flag; if it's there I do `$(element.form).valid();`, otherwise I do `fields.valid();`. – Christof Mar 12 '12 at 16:19
  • Can anyone explain what is actually happeneing here? I have a fairly similar rule, which has worked but in which we hadn't addressed the revalidation problem (other fields in group still marked as invalid). But now I'm experiencing that the form submits even if invalid. If the grouped fields are valid then it does not enter the submithandler and if invalid it enters invalidHandler but submits anyway! I'd say this is a fairly serious bug in the validation plugin? That a rule returns valid applies only to that rule (not even the whole field) so why is an invalid form submitting? – Adam Oct 01 '12 at 14:39
  • I have investigated further and it is fields before the group which do not validate proeprly. I have asked this as a seperate question (with a partial workaround I have discoivered): http://stackoverflow.com/questions/12690898/jquery-validate-and-require-from-group-only-validates-other-fields-after-the-gro – Adam Oct 02 '12 at 13:12
4

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."));
K M
  • 3,224
  • 3
  • 21
  • 17
4

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;(!)"));
3

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!

Michael Gundlach
  • 106,555
  • 11
  • 37
  • 41
  • Thanks for the suggestions - I have clarified the variable names and broken down the code to be a bit more readable. – Nathan Long Aug 20 '09 at 15:24
3

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.

squarecandy
  • 4,894
  • 3
  • 34
  • 45
  • There's no point in this answer since this method/rule was integrated into the plugin back in April 2012. – Sparky Feb 26 '13 at 16:38
  • I have the same issue that Rocket Hazmat has with the method that now ships with validator. It validates that one group of fields, but no other fields using other methods are validated. This answer is an attempt to solve that issue. If you have a better solution please let me know. – squarecandy Mar 02 '13 at 23:32
  • Until the developer permanently fixes the issue, rather than add to any confusion, I simply recommend whatever temporary solution is being endorsed here: https://github.com/jzaefferer/jquery-validation/issues/412 – Sparky Mar 02 '13 at 23:58
2

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.

Andrew Roazen
  • 511
  • 4
  • 4
1

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."));
gen_Eric
  • 223,194
  • 41
  • 299
  • 337
  • Works making the other fields validate again, but now, when all fields of a group are marked as invalid and you fill one out, only that one gets validated. At least for me? – Christof Mar 09 '12 at 07:27
  • @Chris - see my new answer that builds on this one and addresses this. – squarecandy Feb 26 '13 at 00:34
0

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.

Rinat
  • 56
  • 1
  • 6
  • You're right - I didn't consider the noconflict situation. I may update that in the future, but you can easily do a find-and-replace if you like. For your second question, I don't see the same problem. With a quick test, if one field from a group is required, as soon as I type anything, the whole group passes that rule. If more than one is required, as soon as the last required one is filled and loses focus, the whole group passes. Is that what you see? – Nathan Long May 12 '10 at 16:35
  • hmm for some reason markup is screwed up and i had no success in fixing it – Rinat May 12 '10 at 22:57
  • Rinat - can you simplify and narrow down the problem? Try using my code on a simpler form that doesn't need the noconflict changes. Do the simplest form that you can possibly test it on, and get that working first. – Nathan Long May 13 '10 at 22:00