comecme
comecme

Reputation: 6386

Custom validator directive combined with other directive fires multiple times

I have a custom validation directive iban which checks if the value is a valid (Dutch) IBAN. That validator needs the value to be uppercased. I also have an uppercase directive which changes a value to uppercase. I want to combine both on one input element:

<input type="text" ng-model="iban" name="iban" capitalize="" iban="" />

I've created a jsfiddle demonstrating the situation.

I am struggling with the correct order of execution. If the user types in a value, I want capitalize to fire first, so the iban validator receives an uppercased value. If the model value is set from code, I also want to uppercase it first.

When the user types in a lowercase character, the uppercase directive calls ctrl.$setViewValue to set the view value. This triggers another run through the parsers. So both the uppercase directive and the iban directive are executed twice. The console log shows:

parsers.capitalize: nL12HHBA0429672071
  uppercasing: nL12HHBA0429672071 => NL12HHBA0429672071, setting view value
parsers.capitalize: NL12HHBA0429672071
  uppercasing: NL12HHBA0429672071 already uppercased
parsers.iban: NL12HHBA0429672071
  setting validity to: true
  returning NL12HHBA0429672071
parsers.iban: NL12HHBA0429672071
  setting validity to: true
  returning NL12HHBA0429672071

I would think it is not the intention to loop through your parsers multiple times.

Another problem is when I set the value from code to an invalid IBAN that's already uppercased (last link in my fiddle). In that case, the uppercase directive won't have to do anything. The iban directives formatter will set validity to false, and just return the value. If it's an lowercase invalid IBAN, the uppercase directive will call setViewValue, causing the IBAN directives parser code to execute which will return undefined. so that situation will change the model's value to undefined.

Am I making things too complicated? Should I try to create just an iban directive which will make sure that an uppercased value gets stored in the model when the user enters a valid lowercased iban value? Should I just keep the lowercased value in the model if it set from code? And maybe just use style="text-transform: uppercase" on the element to always show the value as if it is uppercased? A disadvantage would be that if the model is set to a valid but lowercased value, the form will show the uppercased value, which would be valid, while the model value is actually invalid.

Upvotes: 1

Views: 1552

Answers (1)

New Dev
New Dev

Reputation: 49590

There is definitely some complexity here. And in playing around with, there is some Angular weirdness as well (at least in my eyes - I'll get to that).

One complexity introduced here is that your capitalize $formatter actually changes the model value. I think this goes against the intent of the formatter function (transforming the value in model -> view direction). The View (and the formatter via its directive lives in the View) should only change the model when the change originates from the View. This keeps the model as the source of truth, and if it is set to an invalid value, then so be it - the validity should be reflected in the View, but it should not try to "fix" the model.

With this in mind, let's also use $validators for validation (rather than $parsers/$formatters pipeline):

.directive("iban", function(){
  return {
    require: "?ngModel",
    link: function(scope, element, attrs, ngModel){
      if (!ngModel) return;

      ngModel.$validators.iban = function(modelValue, viewValue){

        // after parser ran, validate the resulting modelValue
        return validate(modelValue);
      };

      function validate(val){
         return modelValue === "VALID IBAN"; // for the sake of example
      }
    }
  };
});

$parsers (changing the model value) and $formatters (changing the view value) are called before $validators run.

Another complexity though (and what seems like an Angular's weirdness) is that your capitalize formatter can make the $viewValue to become valid for an invalid $modelValue. This on its own behaves correctly - it formats the $viewValue and keeps the validity as false (since the model is false). However, if you now change the model to the currently-set (and valid) $viewValue, then Angular decides to skip (src) the validators (since it finds no difference between new and old $viewValues), and so the validity never becomes valid despite both the model and view values being valid. Same, for valid-to-invalid case (the invalid low case value never invalidates the model).

This is, however, a rare case, and if coded properly should be avoided altogether. Why? Because the model should rarely (if ever) assume invalid values and should operate within the valid range.

ng-model ensures this (by default, unless you allowInvalid) by setting the model to undefined for invalid values.

So, for your question, decide whether low case IBAN is considered invalid in the ViewModel that you defined:

  • If low case is invalid, then never assign a low case value to your ViewModel iban property - plunker
  • If low case is valid, then do case-insensitive iban validator - plunker

Upvotes: 1

Related Questions