1

I ran into a problem when I want to check whether the mail has been registered or not in the db, but only on the blur event ( I dont want it to check on every change ). So I wrote a directive that I can re-use in other places in my app. But the problem is the directive keeps checking even when I still not blur out, Here is my plnkr for reference: http://plnkr.co/edit/eUPFxIc78Wkl4mCX6hrk?p=preview

And here is my directive code:

app.directive('checkEmail', function(userService){
return{
    restrict: "A",
    require:'ngModel',
    link: function( scope, ele, attrs, ctrl ){

        ele.bind('blur', function(){
            console.log("Run in blur!");

            ctrl.$parsers.unshift(function( email ){
                console.log("Email is ", email);

                // Checking to see if the email has been already registered
                if( ele.val() && userService.isDuplicateEmail(email) ){
                    ctrl.$setValidity('isDuplicatedEmail', true );                        
                }else{
                    ctrl.$setValidity('isDuplicatedEmail', false );
                }
            });

        })
    }
}

})

Sorry I'm new to angular and this simple task already drives me nuts. PLease take a look at my directive and tell me what I can do to fix this problem. Thanks in advance

Duc Hong
  • 1,149
  • 1
  • 14
  • 24

4 Answers4

3

Please see here for working sample http://plnkr.co/edit/lT9kO4nU0OeBG3g8lULG?p=preview

Please don't forget to add scope.$apply to update your UI

app.directive('checkEmail', function(userService) {
  return {
    restrict: "A",
    require: 'ngModel',
    link: function(scope, ele, attrs, ctrl) {

      ele.bind('blur', function() {
        scope.$apply(function() {
          console.log("Run in blur!");
          // Checking to see if the email has been already registered
          if (userService.isDuplicateEmail(scope.email)) {

            ctrl.$setValidity('isDuplicatedEmail', false);


            return scope.email;;
          } else {

            ctrl.$setValidity('isDuplicatedEmail', true);

            return scope.email;
          }
        });


      })
    }
  }
})
sylwester
  • 16,498
  • 1
  • 25
  • 33
  • thanks for the specific detail and it works like a charm. But can you explain why when you put the code inside the scope.$apply and it works ? before I posted this question, I had tried something similar to your solution, I put the setValidity code in the beginning and then scope.$apply(); in the end. I thought it will run the $digest cycle one more to update UI but at that time I was wrong. Can you clear me up with that ? – Duc Hong Sep 08 '14 at 14:58
  • @DucHong more likely you can do that in both ways. I just do that to make code more clean. – sylwester Sep 08 '14 at 15:20
  • actually in this situation I cannot call a scope.$apply in the end, I thought it has something to do with the scope related, here is the modified version from your code, [http://plnkr.co/edit/lgDiHvqtvhY1dROUWLDF?p=preview](http://plnkr.co/edit/lgDiHvqtvhY1dROUWLDF?p=preview) – Duc Hong Sep 08 '14 at 15:51
  • 1
    @DucHong your scope.$apply was after 'return' so was never called, that is please see here http://plnkr.co/edit/8IjT5En1TN7w0lNpTUNa?p=preview – sylwester Sep 08 '14 at 15:54
1

If you only need to check the email when blurred, then you don't need to pass it inside the ng-model's $parsers array. Simply change the validity when it is blurred.

DEMO

Something like this:

   ele.bind('blur', function(){
      ctrl.$setValidity('isDuplicatedEmail', ctrl.$viewValue && userService.isDuplicateEmail(ctrl.$modelValue));
      scope.$digest();
   });

Alternatively, if you are checking the email asynchronously from a server then this may not work in to your advantage. The following example is a better answer:

DEMO

The service below, requests for a list of emails from the server and resolving the promises whether the email exists(true) or not(false).

app.service('userServiceAsync', function($http, $q) {

  this.isDuplicateEmailAsync = function(email) {
    var deferred = $q.defer(), i;

    $http.get('users.json')
      .success(function(users) {
        for(i = 0; i < users.length; i++) {
          if(users[i].email == email) {
            deferred.resolve(true);
            return;
          }
        }
        deferred.resolve(false);
      })

      .error(function() {
        deferred.resolve(false);
      });

    return deferred.promise;
  };

});

The directive below, sets the isDuplicatedEmail validity key to true or false, depending on the resolved value of the userServiceAsync.isDuplicateEmailAsync () method. As a bonus, I added a determinant key within the ngModelController's object to check if the asynchronous request is still on going or not (using the __CHECKING_EMAIL key).

app.directive('checkEmailAsync', function(userServiceAsync) {
  return {
    require: 'ngModel',
    link: function(scope, elem, attrs, ctrl) {
      elem.bind('blur', function() {
        ctrl.__CHECKING_EMAIL = true;
        userServiceAsync.isDuplicateEmailAsync(ctrl.$viewValue).then(function(hasEmail) {
          ctrl.$setValidity('isDuplicatedEmail', !hasEmail);
        })['finally'](function() {
          ctrl.__CHECKING_EMAIL = false;
        });
      });
    }
  }
});

HTML

  Check Email Asynchronously
  <input type="text" name="emailAsync" placeholder="Email" ng-model="emailAsync" check-email-async required />
  <div ng-show="registerForm.emailAsync.__CHECKING_EMAIL">
    Checking Email...
  </div>
ryeballar
  • 29,658
  • 10
  • 65
  • 74
  • Thank you for the support, actually I will need an asyn call in my real app as you said. But I have some other things to ask from your answers 1) When should I use `$parsers`, I've read some tutorials about custom validation and all they used is this `$parsers` or `$formatters` thing, 2) Checking out your async example, as the error output still doesn't show, I've changed the out to something like `Email already used` and using scope.$apply() for the UI update, but nothing changes. Any ideas why ? – Duc Hong Sep 08 '14 at 14:48
  • `$parsers` are used to format text from the view(the input value) to the model(the $scope value). As for the async problem, I have fixed it, use `isDuplicatedEmail`, check the second DEMO again. – ryeballar Sep 08 '14 at 15:51
  • thanks, but can you check again the second demo link ? Seem like it's still the old version and the error output still doesn't show when the email is duplicated. Thanks. I changed a little bit like this and it works, `ctrl.$setValidity('isDuplicatedEmail', **!hasEmail**);` – Duc Hong Sep 08 '14 at 16:24
  • I've edited it, its the same as what you mentioned in your comment now. – ryeballar Sep 09 '14 at 01:04
1

$parser.unshift is executed when the model changes, but you really wanted "blur",... either check ngModelOptions' "updateOn" or remove unshift...

you can also use ctrl.$viewValue.

        ele.bind('blur', function(){
            console.log("Run in blur!");

            //ctrl.$parsers.unshift(function( email ){
                email = ctrl.$viewValue;
                console.log("Email is ", email);

                // Checking to see if the email has been already registered
                if( !!email && userService.isDuplicateEmail(email) ){
                    ctrl.$setValidity('isDuplicatedEmail', true );

                    return email;;
                }else{
                    ctrl.$setValidity('isDuplicatedEmail', false );

                    return email;
                }

            //});

        })
sss
  • 1,259
  • 9
  • 23
  • thanks for the comment, that `ngModelOptions` is really new to me. but can you explain more clearly about the way we use `$parser` or `$formatter` thing? I've read some tutorials about custom validation on directives and most of them use these in their custom functions. I thought I was on the right path but it was not – Duc Hong Sep 08 '14 at 15:04
  • 1
    this might help... http://stackoverflow.com/questions/22841225/angularjs-ngmodel-formatters-and-parsers – sss Sep 10 '14 at 05:41
1

@ryeballar , sorry but this still doesn't look right to me. I've modified your plnk so that it just works on the async. So the confusion I have is this line ctrl.$setValidity('isDuplicatedEmail', !hasEmail);, you can visit new plnk here

That boolean value doesn't seems to be right if I dont put a "!" before, you can check it in the console log and see its value, like when an email is not duplicated, the service return false, then we set the validity to "TRUE" in order to make it look "RIGHT".

Here is the directive:

elem.bind('blur', function() {
    ctrl.__CHECKING_EMAIL = true;
    userServiceAsync.isDuplicateEmailAsync(ctrl.$viewValue).then(function(hasEmail) {
        console.log("hasEmail: ", hasEmail);
        console.log("!hasEmail: ", !hasEmail);

        scope.hasEmail = hasEmail;
        // HERE IS THE CONFUSION, WHY !hasEmail works correctly ????
        // THIS WILL GIVE THE WRONG ANSWER
        // ctrl.$setValidity('isDuplicatedEmail', hasEmail);

        // THIS WILL GIVE THE RIGHT ANSWER
        ctrl.$setValidity('isDuplicatedEmail', !hasEmail);


    })['finally'](function() {
      ctrl.__CHECKING_EMAIL = false;
    });
  });
Duc Hong
  • 1,149
  • 1
  • 14
  • 24