When Angular directive's name really matters


I've just come across a strange behavior from Angular:

Here's the scenario:

In a registration form, I want to check for email uniqueness (through an http call to server).
Thus, I created a directive called emailUnique whose client code is:

<form name="form" novalidate>
<!-- some other fields -->
<input name="email" type="email" ng-model="user.email" required email-unique/>
</form>

For the rest of the post, let's suppose that user is typing: michael, that is clearly not a valid mail.

Let's take a look at the interesting portion of my directive code, triggering the behavior I'm interested to:

angular.module('directives.emailUnique', [])
    .directive('emailUnique', function () {
        return {
            restrict: 'A',
            require: 'ngModel',
            link: function (scope, el, attrs, ctrl) {
                ctrl.$parsers.push(function (viewValue) {
                    console.log(viewValue);   //What do you expect here for viewValue? answer below
                });
            }
        };
    });

Before giving the answer, at first glance, the response would logically be:

undefined 

Why? Because:

  • We precise the type="email" attribute and not simply type="text"
  • michael isn't a valid mail.
  • Angular's compiler is supposed to conform to classic HTML behavior.

After testing it, the answer is undefined as expected. My complete directive's logic will be based on that and the whole works fine.

Now, let's rename the directive: emailUnique becoming somethingUnique.
Client being now:

<input name="email" type="email" ng-model="user.email" required something-unique/>

Surprise: the console.log(viewValue) is now displaying: michael, not undefined ...

Clearly, starting with email for the name has a strange effect when dealing with an email field in this case.

My question is simple: Is there a good reason? A possible bug? Might I misunderstand some notion?

Some further precisions:

  • The Angular's documentation about Email field with angular does not present some email attribute that could interfere with email-unique. Indeed, it's based on the type="email"
  • I came across the same issue whether the form's novalidate attribute is present or not.

The issue is the directive's priority. Since you are dependent on the timing of when your parsers are added you want to set the directive's priority- that will ensure the timing you need.

In your demo the somethingUnique directive is running before validation has been added to the parsers list (it ends up the middle of 3 parsers). Whereas with emailUnique it's added after.

Setting the priority of your directive to something over 0 ensures it fires after emailValidation giving you undefined always (noting from the $compile docs: "post-link functions are run in reverse order"). To confirm this you can force emailUnique to fail by setting it's priority to something less than 0.

So this fixes the problem:

.directive('somethingUnique', function () {
    return {
        restrict: 'A',
        require: 'ngModel',
        priority: 100,
        link: function (scope, el, attrs, ctrl) {
            ctrl.$parsers.push(function (viewValue) {
                console.log(viewValue);
            });
        }
    };
});

Updated plunker

Update on the name issue: It appears that Angular processes directives with the same priority in alphabetical order. So homethingUnique acts like emailUnique since both come before input while jomehtingUnique behaves like somethingUnique- running after input.

But Angular's docs say: "The order of directives with the same priority is undefined." So we can't count on alphabetical order.