1
votes

I try to add validation dynamically but validate method returns only the first error. My goal is to parse a json with all validation rules from the server and add validation dynamically.

my html:

<input id="myInput1" value.bind="myInput1 & validateOnChangeOrBlur" errors.bind="myInput1Errors"/>
<input id="myInput2" value.bind="myInput2 & validateOnChangeOrBlur" errors.bind="myInput2Errors"/>
<button click.delegate='validate()'>validate</button>

<ul if.bind="controller.errors">
  <li repeat.for="error of controller.errors">
    ${error.message}
  </li>
</ul>

My .ts:

attached() {
      let rules = [];
      //my real code is a for loop for adding dynamically
      rules.push(ValidationRules
                        .ensure('myInput1')
                        .required()
                        .rules[0]);
      rules.push(ValidationRules
                        .ensure('myInput2')
                        .required()
                        .rules[0]);
     this.controller.addObject(this, rules);
    }

 validate() {
          this.controller.validate()
                .then(result => {
                    if (result.valid) {
                        //
                    } else {
                       //
                    }
                });
        }

When I blur (tab navigation), validate method shows errors correctly, stacking them one after another. When I call validate, it shows only the first one.

However, if I do this statically, it works (both blur and validate):

ValidationRules
            .ensure('myInput1')
            .required()
            .ensure('myInput2')
            .required()
            .on(this);

So what’s wrong with adding it dynamically?

I created a gistrun for that: https://gist.run/?id=c60ee8a86c9c473b97e930e117fd68df

Thanks.

1

1 Answers

1
votes

With the way you're dynamically creating them, you're unintentially creating sequenced rules. I've also found a very subtle bug thanks to your issue. I'll explain below. First, there's two possible solutions:

Solution 1

Make rules an array of arrays, and add the nested array items.

let rules = [[]];
//my real code is a for loop for adding dynamically
rules[0].push(ValidationRules
                  .ensure('myInput1')
                  .required()
                  .rules[0][0]);
rules[0].push(ValidationRules
                  .ensure('myInput2')
                  .required()
                  .rules[0][0]);
this.controller.addObject(this, rules);

Solution 2

Use Rules.set (this is still "wrong", I'll explain below why it works anyway)

let rules = [];
//my real code is a for loop for adding dynamically
rules.push(ValidationRules
                  .ensure('myInput1')
                  .required()
                  .rules[0]);
rules.push(ValidationRules
                  .ensure('myInput2')
                  .required()
                  .rules[0]);
Rules.set(this, rules);

The cause

The outer array is an array of rule sequences (which are individually also arrays). The inner array is the list of rules within that sequence.

By adding .rules[0] to an array, you end up with an array of rule sequences that each have a single rule:

rules[0][0]
rules[1][0]
rules[2][0]
rules[3][0]

However when you chain calls to the same rule builder, it does the correct thing internally and .rules will be an array with one sequence that contains all the individual rules:

rules[0][0]
rules[0][1]
rules[0][2]
rules[0][3]

Sequences

Sequences allow you to execute rules in a specific order, where each consecutive sequence is only evaluated when all previous sequences evaluated to valid. So the sequences you are creating actually behave as intended: the first property turns out to be invalid, and the rest isn't evaluated.

FYI, the "normal" way of creating sequenced rules is by using .then() on the rule builder. As long as you don't use .then(), you'll always have a single sequence that has the index of 0.

How the rules are stored

When you use controller.addObject(this, rules) the rules are stored in a map on that controller. Only that controller will know about those rules.

On the other hand, Rules.set(), stores the rules in a metadata object (property __rules__) on the prototype of the object you give it. The finalizer .on() stores them the same way (it just calls Rules.set()). This makes the rules effectively available to all controllers.

How differently stored rules are processed

  1. When you call .validate() on a controller, it will first look for "local" rules stored via .addObject() and pass those to the validator via .validateObject().

This is what happens in your case. The rules are processed in one pass, and the sequence behaves as it should.

The below is technically not relevant for your issue, but I'm documenting it here for reference.

  1. Then it goes through the registered bindings (added by the binding behaviors) and for each individual property it will call .validateProperty() on the validator if the object is not stored on the controller.

So this step does not happen in your case, but it does in my "solution 2". Rather than the sequence being run once on the object, it's run once per individual property, effectively bypassing the behavior of sequences to some extent (they still apply on property level, just not on object level)

So that was an interesting discovery I must say :)