views:

3823

answers:

8

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 is a rule like this:

partnumber: {
   require_from_group: [2,".productinfo"]
  }
//The partnumber input will validate if 
//at least 2 `.productinfo` inputs are filled

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) {
    numberRequired = options[0];
    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 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).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!

+2  A: 

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
Thanks for the suggestions - I have clarified the variable names and broken down the code to be a bit more readable.
Nathan Long
A: 

I'm having trouble to adopt your solution, could you post a link to a demo?

Urs
Urs - I don't have a demo online right now. Have you got jQuery Validate itself working yet? This demo was the one that helped me see how to use it (click to 'show script used on this page'): http://jquery.bassistance.de/validate/demo/milk
Nathan Long
+3  A: 

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
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
I updated my example to show more clearly what I mean.
Nathan Long
A: 

That works really well as is. But im having a little trouble with masked input: http://digitalbush.com/projects/masked-input-plugin/

It appears as though the masked input is firing after your program and making it valid even if you are just tabbing through it. Im a javascript newbie so I dont know if that is correct or not.

Sorry, I'm not familiar with that plugin. My first thought is maybe that plugin is inserting characters into the field, and your rule says that if it's no longer empty, it must be valid. In that case, you could change my code from `if ($(this).val())` to check for a particular format. If that doesn't help you, I'd recommend you start a new StackOverflow question about the problem you're having, and link to this page to explain that you're using my code with the masked input.
Nathan Long
Yes I was stepping through using firebug and it picks up the mask before it is removed. phone eg: (__) ____-____. THe masked input does delete after you move out but not before your program marks it as clear. I Will have a fiddle with it to see if i can get it working.Also what would be a cool idea would be a check the cells after leaving to see if they all need to be re marked bad. Because currently the only cell that get marked bad is the one you are on.
Yes - I think that would be cool too. However, I don't know a way to do it. I asked this question: http://stackoverflow.com/questions/1378472/jquery-validate-can-i-re-validate-a-group-of-fields-after-changing-one
Nathan Long
A: 

I'm using addClassRules() route as shown by Gerasimos. But the message (Please fill out at ...) keeps repeating per input field. Is there a way to make it appear only once per group?

Thanks for the excellent example.

Naveen
It's not because of how you're doing it. Currently this method does put the message "please fill out at least X of these" beside each input in the group. The code would have to be reworked to show it only once per group, and those inputs would have to be in a common div or something so that the user sees them as a group. I may try that in the future. If you do it first, please post the code as a reply.
Nathan Long
Also - you're welcome. :)
Nathan Long
If you want to try coding this, one idea would be to add another class to all the group members, which causes them to be highlighted, and show the error message only for the first group member. If you had more than one validation group on a page, you'd have to find a way to differentiate those two groups (different colors?). Another idea is to add asterisks; you could do one asterisk beside each member of the first group, two for the second, etc.
Nathan Long
+1  A: 

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
Nice - thanks for commenting.
Nathan Long
A: 

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
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
No, i see error message staying.here's my setup:`var $j = jQuery.noConflict();``$j(function() {``$j("#vform").validate({``rules: { email: {``required: true,``email: true``},``dummy: { require_from_group: [1,".vote"] }``},``submitHandler: function(form) {``form.submit();``}``);`dummy is name and id of hidden input (may be here's the problem but i had no success in `vote: { require_from_group: [1,".vote"] }`edit: for some reason markup is screwed up in comments, i'm new to this site
Rinat
hmm for some reason markup is screwed up and i had no success in fixing it
Rinat
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
A: 

I Just spent several hours on this. The section that re-validates other validation in the group for some reason doesn't show validation errors on the rest of the page. I have lots of item, like 5 required fields, then a group of checkboxes and radiobuttons that need one selected.

I made it work, and if focus triggers on the required fields they show, but if you submit the form just doesn't go and you see no messages.

I commented out the section and the page works normally. Luckily I don't have further validation within the group. But it is driving me nuts and I am wondering if anybody could explain why it happens and possibly have a quick solution.

rontoub