1

I am trying to create a survey that employees can fill out after completing a phone call that will be used to track what products or services were sold during the interaction. The form can have an infinite number of accounts appended to the account-wrapper and each account can have an infinite number of loans appended to its loan wrapper. I am using Jörn Zaefferer's validation plugin to validate the survey before it is submitted; however, the plugin only validates the account most recently added to the form.

How do I get the plugin to validate the entire form?

What I Have Learned: I am dumb and checkboxes should use REQUIRE and never REQUIRE FROM GROUP

Here is the code I use for my form.

<form id="survey" method="GET" action="/processSurvey">
    <h1>What products/services were completed during the call?</h1>
    <div id="account-wrapper">
    </div><!-- End Account Wrapper -->
    <button type="submit">Submit</button> <button type="button" onclick="addAccount();">Add Account</button>
    <input type="hidden" name="callID" value="0" />
 </form>

I dynamically append accounts to the account-wrapper using the handlebars template below:

<script id="account-template" type="text/x-handlebars-template">
  <div id="account-{{id}}" class="account">
    <div class="account-title ui-state-active">Account<button type="button">Remove Account</button></div>
    <div class="service-wrapper">
      <div class="service-container">
        <span class="title">Account</span>
        {{#each account}}
          <span class="service-option">
            <input type="checkbox" class="service service-group-{{accountID}}" name="account[{{accountID}}][service][]" value="{{value}}" /> {{html}}
          </span>
        {{/each}}
      </div>
      <div class="service-container">
        <span class="title">Savings</span>
        {{#each savings}}
          <span class="service-option">
            <input type="checkbox" class="service service-group-{{accountID}}" name="account[{{accountID}}][service][]" value="{{value}}" /> {{html}}
          </span>
        {{/each}}
      </div>
      <div class="service-container">
        <span class="title">Checking</span>
        {{#each checking}}
          <span class="service-option">
            <input type="checkbox" class="service service-group-{{accountID}}" name="account[{{accountID}}][service][]" value="{{value}}" /> {{html}}
          </span>
        {{/each}}
      </div>
      <div class="service-container">
        <span class="title">Cards</span>
        {{#each cards}}
          <span class="service-option">
            <input type="checkbox" class="service service-group-{{accountID}}" name="account[{{accountID}}][service][]" value="{{value}}" /> {{html}}
          </span>
        {{/each}}
      </div>
      <div class="service-container">
        <span class="title">Other</span>
        <textarea class="service service-group-{{id}}" name="account[{{id}}][service][]"></textarea>
      </div>
      <div class="error-container"></div>
    </div>
    <div id="loan-wrapper-{{id}}" class="loan-wrapper">
      <div class="loan-title">Loans<button type="button" onclick="addLoan({{id}});">Add Loan</button></div>
      <div class="loan-container"></div>
    </div>
  </div>
</script>

Entire JavaScript:

$(document).ready(function(){
    addAccount();
  });

  $(":button").button();
  var loanCounter = 0;
  var accountCounter = 0;

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

if(!$(element).data('being_validated')) {
      var fields = $(selector, element.form);
      fields.data('being_validated', true);
      fields.valid();
      fields.data('being_validated', false);
}
return validOrNot;
  }, $.format("Please select at least {0} product/service above (or fill in other)."));

  $.validator.addMethod("ignorePlaceholder", function(value,element){
      return this.optional(element) || (value !== element.placeholder);
  },"This field is required.");

  $.validator.addClassRules("required", {
    ignorePlaceholder: true,
    required: true
  }); 

  var validator = $("#survey").validate({
    debug: true,
    errorClass: "error",
    validClass: "valid",
    submitHandler: function(form){
      form.submit();
    },
    invalidHandler: function(){
      alert(validator.numberOfInvalids());
    },
    errorPlacement: function(error, element){
      if($(element).hasClass("service")){
        error.appendTo($(element).parents(".service-wrapper").children("div.error-container"));
      }
    },
    rules: {
    }
  });

  function addAccount(){
    var newAccount = {
      id:accountCounter,
      account:[
        {value:"1",html:"New Account",accountID:accountCounter},
        {value:"13",html:"eStatements",accountID:accountCounter}
      ],
      savings:[
        {value:"2",html:"My Choice",accountID:accountCounter},
        {value:"3",html:"Money Market",accountID:accountCounter},
        {value:"4",html:"Individual Reitrement Account",accountID:accountCounter},
        {value:"5",html:"Individual Retirement Account (Certificate of Deposit)",accountID:accountCounter},
        {value:"6",html:"Certificate of Deposit",accountID:accountCounter}
      ],
      checking:[
        {value:"7",html:"Prosper Checking",accountID:accountCounter},
        {value:"8",html:"ePerks Checking",accountID:accountCounter},
        {value:"9",html:"Protect Checking",accountID:accountCounter},
        {value:"10",html:"Basic Checking",accountID:accountCounter}
      ],
      cards:[
        {value:"11",html:"Credit Card",accountID:accountCounter},
        {value:"12",html:"Debit Card",accountID:accountCounter}
      ]
    }

    var template = Handlebars.compile($("#account-template").html());
    var result = template(newAccount);
    $("#account-wrapper").append(result);

    $("#account-wrapper .account:last :button").button();
    $("#account-wrapper .account:last div.account-title :button").click(function(){
      $(this).parents("div.account").remove();
    });

    $("#account-"+accountCounter).placeholder();
    $("html, body").animate({scrollTop: $("#account-"+accountCounter).offset().top},1000);

    $(".service-group-"+accountCounter).each(function() {
      $(this).rules('add', {
        require_from_group: [1, ".service-group-"+accountCounter]
      });
    });

    accountCounter++;
  }

Notice how the top form is invalid because I do not have an option checked. If I add an identical account template and check an option, the form will submit.

jsFiddle Error

jsFiddle <-- Link to working replica of the problem. Notice how the form only cares that the most recently added account is valid.

Neve12ende12
  • 1,154
  • 4
  • 17
  • 33
  • @Barmar The accountCounter variable ensures that each group of inputs for an account has a unique index. So while individual inputs within an account might share a name, each account group will ALWAYS be unique. – Neve12ende12 Mar 10 '14 at 17:15
  • 1
    The validator doesn't care about the account counter, all it cares about is the name. You need to give each input a unique name, as explained in the question I linked to. – Barmar Mar 10 '14 at 18:07
  • What @Barmar said. Each input considered for validation must have a _unique_ `name`... this is the only way the plugin keeps track of everything. – Sparky Mar 10 '14 at 19:03
  • @Barmar So are you telling me name="account[0][service][]" and name="account[1][service][]" are the same name? According to the question you linked to, they are not the same name. – Neve12ende12 Mar 10 '14 at 19:16
  • I'm saying that all the inputs with `name="account[0][service][]"` are the same. I assume this appears multiple times, otherwise why would you be using `[]` at the end? – Barmar Mar 10 '14 at 19:49
  • Ok, just to verify that this has nothing to do with the names, I have went through and added the unique value of each checkbox to the name so I have ONLY UNIQUE NAMES like: account[0][service][0], account[0][service][1], account[0][service][2], account[1][service][0], account[1][service][1], account[1][service][2]. **The form still only cares that the last account is valid.** – Neve12ende12 Mar 10 '14 at 20:02
  • Please post an example of the _rendered_ HTML markup so I can test your claim in my demo. – Sparky Mar 10 '14 at 20:07
  • Also, where is your `.validate()` call? – Sparky Mar 10 '14 at 20:20
  • 1
    Why are you using a custom `require_from_group` method? This rule already exists in the `additional-methods.js` file. Just be sure to use the latest one (v1.11.1). – Sparky Mar 10 '14 at 20:41
  • AFAIK, you don't need to create an `ignorePlaceholder` method as that's already the default behavior. The `value` of the element is the only thing validated... not the `placeholder` attribute. – Sparky Mar 10 '14 at 20:44
  • Because passing messages:{require_from_group: $.format("Please select at least {0} product/service above (or fill in other)."));} in with .validate will not overwrite error message. – Neve12ende12 Mar 10 '14 at 20:46
  • You do not need to use the `submitHandler` callback if it only contains `form.submit()` as that's the default behavior. – Sparky Mar 10 '14 at 20:46
  • 1
    I fail to see how `messages` relates to that. You can just as easily overwrite the default `messages` with the `.rules()` method too. – Sparky Mar 10 '14 at 20:47
  • Submit handler is there because I plan to use ajax. ignorePlaceholder is for the loans (not included) because they have default values that will cause the required rule to validate to true when it should not. – Neve12ende12 Mar 10 '14 at 20:49
  • 1
    Ok, reasonable explanations, but yet mostly superfluous code as far as this problem is concerned. Perhaps you can condense this down into ***just enough*** code that reproduces the problem. Strip out the templates, strip out most of the inputs, strip out the custom methods... just make a small dummy form repeated a couple times with similar unique naming. As it stands, it would take a huge amount of my time to put this all into a jsFiddle. – Sparky Mar 10 '14 at 20:55
  • **Voting to reopen. Upon closer examination (and the OP's jsFiddle), it's clear this is NOT a duplicate of anything.** – Sparky Mar 12 '14 at 16:07

1 Answers1

1

I think I finally see what's going wrong here.

You should not be using the .addClassRules() method as that's only for creating a compound rule assigned to a class.

Rather, you should be using the .rules('add') method, which is for dynamically declaring rules on your new elements.

function addAccount(){
    var newAccount = {
      // your code
      ....
    }

    // your code
    .....

    // add the rule to every element in the newly created group

    var group = ".service-group-" + accountCounter;

    $(group).each(function() {
        $(this).rules('add', {
            require_from_group: [1, group],
            messages: {
                require_from_group: "this is a custom message"
            }
        });
    });

    accountCounter++;
}

EDIT:

I believe the OP found another bug with the require_from_group method in the complex way he's implementing it.

However, using the standard required rule on a checkbox group, by default, already only requires ONE item from the group. In other words, as long as all checkboxes in the group share the same name (as they would normally do), you'll only be required to check one item.

This is the OP's demo modified to remove the require_from_group method and replace it with a standard required rule. I also modified his template so that all checkbox elements within one account section will share the same name. It seems to be working now:

http://jsfiddle.net/T2pUS/9/

Sparky
  • 98,165
  • 25
  • 199
  • 285
  • I have changed addClassRules to the code you have posted above, but I am still encountering the same problem. I shall paste my entire script above to help (minus the addLoan function, which is irrelevant). – Neve12ende12 Mar 10 '14 at 20:32
  • @JohnHargis, please do, as this should be working. – Sparky Mar 10 '14 at 20:40
  • The last segment of code before the image has been updated to include the entire script – Neve12ende12 Mar 10 '14 at 20:42
  • I got the [jsFiddle](http://jsfiddle.net/T2pUS/1/) started. It doesn't seem to like adding the require_from_group at the bottom of the javascript – Neve12ende12 Mar 11 '14 at 14:48
  • You should also be using the latest versions of both jQuery Validate files, since bugs were fixed in v1.11.1 – Sparky Mar 11 '14 at 19:11
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/49501/discussion-between-john-hargis-and-sparky) jsFiddle has been updates to work – Neve12ende12 Mar 11 '14 at 20:05
  • Test Case - Add a new account. Check nothing in the first account and check something in the second account. You should get "Number of invalids" alert instead of "Form is Valid" alert because something should be required to be checked in the first account before the form can be submitted. Should I attach a replace the screenshot in the question with the jsFiddle that I am getting? – Neve12ende12 Mar 12 '14 at 05:57
  • I have updated the picture in the question to show the exact error I am getting in jsFiddle. (I get this error in IE11, Chorme and Firefox) – Neve12ende12 Mar 12 '14 at 15:19
  • @JohnHargis, yes, I see the alert now. Very strange. I recommend a couple things (these won't fix it yet) to clean up that jsFiddle: 1) Use the `additional-methods.js` (v1.11.1) file instead of writing out your own `require_from_group`. 2) Pass the `validator` keyword as an argument into the `invalidHandler` callback like this `invalidHandler: function (event, validator) {` – Sparky Mar 12 '14 at 15:44
  • 1
    @JohnHargis, it just occurred to me... why are you using the `require_from_group` rule in the first place? In the case of `checkbox` groups, by default, only one checkbox is required when using the standard `required` rule. The key is that all checkboxes in the group must share the same `name`. Tweak your code so that each checkbox group shares a `name` and use the `required` rule instead. You can even forget about using the `rules()` method by simply putting a `required="required"` inside each `input`'s HTML. This will simplify things greatly. – Sparky Mar 12 '14 at 15:52
  • Something like this: http://jsfiddle.net/T2pUS/9/ – Sparky Mar 12 '14 at 15:54
  • You sir, just solved my problem. Would you mind editing this comment into your answer that I will be giving the green checkmark of victory? – Neve12ende12 Mar 12 '14 at 16:02
  • @JohnHargis, I edited my answer and voted to re-open your question as well. – Sparky Mar 12 '14 at 16:08