Angularjs - Checking duplicated email on blur in directive - angularjs

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

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;
}
});
})
}
}
})

$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;
}
//});
})

#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;
});
});

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>

Related

Use an angular directive to activate or deactivate a call to a service

I'm checking whether or not a username already exists when I create a new user. I also want to be able to edit a user, and I'm actually using the same form for this. When the form is in edit-mode, you are not allowed to change the username and the input field will be disabled.
I have a directive that checks with the database if the username is unique, and validates or invalidates the input. This directive should not fire when the form is in edit-mode, because the username is not allowed to be edited, and the form should validate.
<input name="alias"
class="form-control"
type="text"
ng-model="user.alias"
required
ng-model-options="{ debounce: { default : 500, blur: 0 }}"
validate-alias="{{formMode == 'new' ? true : false}}"
/>
And the directive:
.directive('validateAlias', function($http, $q) {
return {
restrict: 'A',
require: 'ngModel',
validateAlias: '=',
link: function(scope, element, attrs, ngModel) {
attrs.$observe('validateAlias', function(){
console.log("validateAlias: " + attrs.validateAlias + ", which is a " + typeof attrs.validateAlias);
//this returns "validateAlias: false, which is a string"
if (attrs.validateAlias === 'true') console.log("should work"); else console.log("n/a");
//this returns "should work"
if (attrs.validateAlias === 'true') {
ngModel.$asyncValidators.username = function (alias) {
return $http.get(aliasurl + alias).then(
function (response) {
if (response.data.length > 0) {
return $q.reject('Username not unique');
} else {
return $q.resolve();
}
}
);
};
}
});
}
};
})
So what happens is that in the edit-mode the first if-statement returns what I expect (false, aka do not validate, and the attribute in the DOM matches this, displaying validate-alias='false'), yet the username is still checked with the database.
Everything works fine when I'm in new-mode, but that's probably just coincidence, not because the code works as it should.
I am doing something wrong, but I can't figure out where...!
Edit:
Right, I've changed things around a little bit. I now have the following directive:
.directive('validateAlias', function($http, $q) {
return {
restrict: 'A',
scope: {
validateAlias: '#'
},
link: function(scope, element, attrs) {
scope.$watch(attrs.validateAlias, function(v){
console.log("validateAlias: " + v + ", which is a " + typeof v);
if (v === true) console.log("the check is active"); else console.log("the check is NOT active");
if (v === true) {
ngModel.$asyncValidators.username = function (alias) {
return $http.get(aliasurl + alias).then(
function (response) {
if (response.data.length > 0) {
return $q.reject('Username not unique');
} else {
return $q.resolve();
}
}
);
};
}
});
}
};
})
Now the $watch does not seem to work... what am I missing?
Final edit:
The DOM had a set variable "false" for the attribute I made... the watch worked fine, but the DOM was generated once, so there was nothing to watch.
Luckily for me, every time the attribute changed, I could reload the DOM as well (virtually switching screens), and so the scope reads the attribute again, and the variable is passed, making the directive do exactly what I want. The code I posted after the first edit is what I used.
I'm not sure if that's the Angular way, but it works for now. When I find a better solution, I'll update the code.
Try this:-
.directive('validateAlias', function($http, $q) {
return {
restrict: 'A',
require: 'ngModel',
scope: {
validateAlias: '=',
}
link: function(scope, element, attrs, ngModel) {
scope.$watch('validateAlias', function(watchData){
console.log("validateAlias: " + watchData + ", which is a " + typeof watchData);
if (watchData === 'true') console.log("should work"); else console.log("n/a");
//this returns "should work"
if (watchData.value === 'true') {
ngModel.$asyncValidators.username = function (alias) {
return $http.get(aliasurl + alias).then(
function (response) {
if (response.data.length > 0) {
return $q.reject('Username not unique');
} else {
return $q.resolve();
}
}
);
};
}
});
}
};
})

Skip $asyncValidators until model is dirty

I use $asyncValidators to validate a field value. The field value is fetched from server with $resource. On page load, the value is empty and the validator sends a request to the server. When $resource has fetched the resource, the field value is populated and the validator sends another request to the server.
I would like to skip these two initial requests since they are useless. How do I do that?
My input field:
<input name="name" class="form-control" ng-model="user.name" validate-name>
My directive:
angular.module('myapp').directive('validateName', function($http, $q) {
return {
require: 'ngModel',
link: function(scope, element, attrs, ctrl) {
ctrl.$asyncValidators.name = function(modelValue, viewValue) {
return $http.post('/validate-username', {username: viewValue}).then(function(response) {
if (!response.data.validUsername) {
return $q.reject(response.data.errorMessage);
}
return true;
});
};
}
};
});
A reasonable condition is for the model to be touched; if it isn't, just return a resolved promise, without accessing the server. I.e.:
ctrl.$asyncValidators.name = function(modelValue, viewValue) {
if( ctrl.$touched ) {
return $http.post('/validate-username', {username: viewValue})....
}
else {
return $q.resolve(true);
}
};

How to implement Firebase in $asyncValidators?

I'd like to enforce unique usernames in my Firebase app and immediately let the user know if the inserted username is already taken or not.
I looked into AngularJS's ngModel since it has a built-in asyncValidator in its controller (example in Custom Validation), but I'm still stuck in get it work.
html
<form name="settings" novalidate>
<div>
Username:
<input type="text" ng-model="user.username" name="username" app-username><br>
<span ng-show="settings.username.$pending.appUsername">Checking if this username is available...</span>
<span ng-show="settings.username.$error.appUsername">This username is already taken!</span>
</div>
</form>
directive
app.directive('appUsername', function() {
return {
require: 'ngModel',
link: function (scope, elm, attrs, ctrl) {
var ref = new Firebase("https://<MY-APP>.firebaseio.com");
ctrl.$asyncValidators.appUsername = function (modelValue, viewValue) {
if (ctrl.$isEmpty(modelValue)) {
// consider empty model valid
return true;
}
var q = ref.child('users').orderByChild('username').equalTo(modelValue);
q.once('value', function (snapshot) {
if (snapshot.val() === null) {
// username does not yet exist, go ahead and add new user
return true;
} else {
// username already exists, ask user for a different name
return false;
}
});
};
};
};
});
Is it possible to use Firebase in an asyncValidator?
Thanks in advance.
Yes, it's possible. However, the issue you're running into isn't Firebase related. Per the Angular docs you linked:
Functions added to the object must return a promise that must be resolved when valid or rejected when invalid.
So you simply need to use a deferred as done in the Angular docs, or use $q(function(resolve, reject) {...}). I've used the latter below.
app.directive('appUsername', function($q) {
return {
require: 'ngModel',
link: function (scope, elm, attrs, ctrl) {
var ref = new Firebase("https://<MY-APP>.firebaseio.com");
ctrl.$asyncValidators.appUsername = function (modelValue, viewValue) {
return $q(function(resolve, reject) {
if (ctrl.$isEmpty(modelValue)) {
// consider empty model valid
resolve();
}
var usernameRef = ref.child('users').orderByChild('username').equalTo(modelValue);
usernameRef.once('value', function (snapshot) {
if (snapshot.val() === null) {
// username does not yet exist, go ahead and add new user
resolve();
} else {
// username already exists, ask user for a different name
reject();
}
});
});
};
};
};
});

angularjs directive validation unique email check before send

html
<input type="email" data-input-feedback="" data-ng-model="user.email" data-unique-email="" required="required" placeholder="Email" class="form-control" id="email" name="email">
js
.directive('uniqueEmail',function (User) {
return {
require:'ngModel',
restrict:'A',
link:function (scope, element, attrs, ngModelCtrl) {
ngModelCtrl.$parsers.push(function (viewValue) {
/*
Is there a way to check if it's a valid email ?
both ngModelCtrl.$valid and ngModelCtrl.$error.email doesn't work
*/
User.isUniqueEmail(viewValue).then(function(data){
ngModelCtrl.$setValidity('uniqueEmail', !data.email);
});
return viewValue;
});
}
};
});
so is there a way to check if it's a valid email
before sending the value to the server ?
UPDATE
ngModelCtrl.$parsers.push
using push() here to run it as the last parser, after we are sure that other validators were run
so only if required and email validation are passed
do the call to check for unique email
END UP
.directive('uniqueEmail',function (User) {
return {
require:'ngModel',
restrict:'A',
controller:function ($scope) {
$scope.isValidEmail = function(){
return $scope.form.email.$error.email;
}
},
link:function (scope, element, attrs, ngModelCtrl) {
var original;
// If the model changes, store this since we assume it is the current value of the user's email
// and we don't want to check the server if the user re-enters their original email
ngModelCtrl.$formatters.unshift(function(modelValue) {
original = modelValue;
return modelValue;
});
// using push() here to run it as the last parser, after we are sure that other validators were run
ngModelCtrl.$parsers.push(function (viewValue) {
if (viewValue && viewValue !== original ) {
if(scope.isValidEmail(viewValue)){
User.isUniqueEmail(viewValue).then(function(data){
ngModelCtrl.$setValidity('uniqueEmail', !data.email);
});
}
return viewValue;
}
});
}
};
});
With compile and priority
.directive('uniqueEmail',function (User) {
return {
require:'ngModel',
restrict:'A',
priority:0,
compile: function compile(tElement, tAttrs, transclude) {
return function (scope, element, attrs, ngModelCtrl) {
var original;
// If the model changes, store this since we assume it is the current value of the user's email
// and we don't want to check the server if the user re-enters their original email
ngModelCtrl.$formatters.unshift(function(modelValue) {
original = modelValue;
ngModelCtrl.$setViewValue(original);
return modelValue;
});
// using push() here to run it as the last parser, after we are sure that other validators were run
ngModelCtrl.$parsers.push(function (viewValue) {
if (viewValue && viewValue !== original ) {
if(scope.isValidEmail()){
User.isUniqueEmail(viewValue).then(function(data){
ngModelCtrl.$setValidity('uniqueEmail', !data.email);
});
}
return viewValue;
}
});
scope.isValidEmail = function(){
return scope.form.email.$isvalid;
}
}
}
}
});
it still doesnt work the value of scope.form.email.$isvalid
is unreiable and seems out of date :(
The order of the parsers is important. I am not sure but your parser may have got registered before the inbuilt parsers and hence gets fired first.
Maybe if you splice the $parsers array and insert your parser at 0 it may work.

Email form validation one at a time

I'm trying to implement form validation for an email field. Validation should do following:
Check if email has been entered over required attribute and display a message if no email has been entered
Check if the email has a valid format (seems to be done automatically without specifying an attribute) and display a message if the format is wrong
Check if the email is unique over a $http.get call and display a message in case the email was found and therefore can't be used
I would like that the first message appears, if the field is empty, the second message appears if the email is invalid and the third message appears, if the email address is found and therefore not unique one at a time.
This works if I try with only the "required" attribute but as soon I add my email-available directive attribute it doesn't check the format of the email any longer and the email-available directive is executed together with the required attribute. Both messages pop up but I would only like that the user sees one message at a time.
I'm using angularjs 1.1.3.
Can somebody tell me what I might me doing wrong?
HTML
<div id="user_mod" class="mod_form" ng-show="userModScreenIsVisible">
<form name="mod_user" novalidate>
<input type="email" name="email" ng-model="user.email" placeholder="E-Mail" required email-available/>
<div class="user-help" ng-show="mod_user.email.$dirty && mod_user.email.$invalid">Invalid:
<span ng-show="mod_user.email.$error.required">Please enter your email.</span>
<span ng-show="mod_user.email.$error.email">This is not a valid email.</span>
<span ng-show="mod_user.email.$error.emailAvailable">This email address is already taken.</span>
</div>
</form>
Directive
directive('emailAvailable', function($http) { // available
return {
require: 'ngModel',
link: function(scope, elem, attr, ctrl) {
ctrl.$parsers.unshift(function(viewValue) {
ctrl.$setValidity('emailAvailable', false);
if(viewValue !== "" && typeof viewValue !== "undefined") {
console.log("variable is defined");
$http.get('/api/user/email/' + viewValue + '/available')
.success(function(data, status, headers, config) {
console.log(status);
ctrl.$setValidity('emailAvailable', true);
return viewValue;
})
.error(function(data, status, headers, config) {
console.log("error");
ctrl.$setValidity('emailAvailable', false);
return undefined;
});
} else {
console.log("variable is undefined");
ctrl.setValidity('emailAvailable', false);
return undefined;
}
});
}
};
});
I see you've already solved your own problem, but I think I can offer you some tips/advice here (I hope):
1) All you needed to do to ensure that your validator was run after the built-in angular validators was push() it onto ctrl.$parsers, rather than unshift() it.
2) To keep your validator from running due to the previously run validators showing it's invalid (i.e. If you don't want to make the Ajax call if the field is already invalidated). You just need to check ctrl.$invalid in an if statement inside of your validator.
3) You'll want to invalidate your form with a separate call to $setValidity() before starting your ajax call, and after it's been received. This way, your form is invalid until the AJAX returns and says whether it's valid or not.
4) This is probably minor, but unless you add a ctrl.$formatter as well, values initially assigned to your object in $scope will not be validated before they're written to the screen. This can be a problem if your form is dynamically added to the screen via routing, ng-repeat, or ng-include with data pre-populated. Generally all validators should have a $parser component (view -> model) and a $formatter component (model -> view).
5) A word of caution. Most all validators will completely remove the value from the model if it's invalid. Becuase you're making an asynchronus call, you're going to have to return the viewValue immediately in your parser function. Usually the parser function will return undefined if the field is invalidated, which prevents invalid data from being in your model.
6) Since the validators have a state that is maintained in your $error object, you'll want to clear it out when this async validator is initially hit. See below.
7) Side Note: In your answer, I noticed you were returning values in your ajax response handlers... that's not going to do anything for you. Because the call is asynchronous, you're effectively always returning undefined from that parser. Does it update your model? I'd be surprised if it did.
Here is how I'd have altered your original directive to make it work as you probably would like:
app.directive('emailAvailable', function($http, $timeout) { // available
return {
require: 'ngModel',
link: function(scope, elem, attr, ctrl) {
console.log(ctrl);
// push the validator on so it runs last.
ctrl.$parsers.push(function(viewValue) {
// set it to true here, otherwise it will not
// clear out when previous validators fail.
ctrl.$setValidity('emailAvailable', true);
if(ctrl.$valid) {
// set it to false here, because if we need to check
// the validity of the email, it's invalid until the
// AJAX responds.
ctrl.$setValidity('checkingEmail', false);
// now do your thing, chicken wing.
if(viewValue !== "" && typeof viewValue !== "undefined") {
$http.get('/api/user/email/' + viewValue + '/available')
.success(function(data, status, headers, config) {
ctrl.$setValidity('emailAvailable', true);
ctrl.$setValidity('checkingEmail', true);
})
.error(function(data, status, headers, config) {
ctrl.$setValidity('emailAvailable', false);
ctrl.$setValidity('checkingEmail', true);
});
} else {
ctrl.$setValidity('emailAvailable', false);
ctrl.$setValidity('checkingEmail', true);
}
}
return viewValue;
});
}
};
});
And... of course, here is a plunker demonstrating it all
I have solved it by removing the required attribute and adding a second directive called email-valid. Also I have removed the setValidity method in the else clause of the emailAvailable directive.
HTML
<form name="mod_user" novalidate>
<input type="email" name="email" ng-model="user.email" placeholder="E-Mail" email-valid email-available/>
<div class="user-help" ng-show="mod_user.email.$dirty && mod_user.email.$invalid">Invalid:
<span ng-show="mod_user.email.$error.emailValid">Please enter a valid email address.</span>
<span ng-show="mod_user.email.$error.emailAvailable">This email address is already taken.</span>
</div>
<br/>
<button class="button" ng-click="hideUserModScreen()">Close</button>
<button class="button" ng-click="updateUserDetails()" ng-disabled="mod_user.$invalid" ng-show="updateUserDetailsButtonIsVisible">Update</button>
<button class="button" ng-click="saveUserDetails()" ng-disabled="mod_user.$invalid" ng-show="saveUserDetailsButtonIsVisible">Save</button>
</form>
Directives
angular.module('myApp.directives', []).
directive('appVersion', ['version', function (version) {
return function (scope, elm, attrs) {
elm.text(version);
};
}]).
directive('emailAvailable', function($http) { // available
return {
require: 'ngModel',
link: function(scope, elem, attr, ctrl) {
ctrl.$parsers.unshift(function(viewValue) {
if(viewValue && viewValue.match(/[a-z0-9\-_]+#[a-z0-9\-_]+\.[a-z0-9\-_]{2,}/)) {
console.log("variable is defined");
$http.get('/api/user/email/' + viewValue + '/available')
.success(function(data, status, headers, config) {
console.log(status);
ctrl.$setValidity('emailAvailable', true);
return viewValue;
})
.error(function(data, status, headers, config) {
console.log("error");
ctrl.$setValidity('emailAvailable', false);
return undefined;
});
} else {
console.log("variable is undefined");
return undefined;
}
});
}
};
}).
directive('emailValid', function() {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
ctrl.$parsers.unshift(function(viewValue) {
if (viewValue && viewValue.match(/[a-z0-9\-_]+#[a-z0-9\-_]+\.[a-z0-9\-_]{2,}/)) {
// it is valid
ctrl.$setValidity('emailValid', true);
return viewValue;
} else {
// it is invalid, return undefined (no model update)
ctrl.$setValidity('emailValid', false);
return undefined;
}
});
}
};
});
I extended Ben Lesh answer a bit, by adding a timeout so that it does not check for every keystroke
app.directive "uniqueEmail", ($http, $timeout) ->
restrict: "A"
require: "ngModel"
link: (scope, elem, attrs, ctrl) ->
return unless ctrl
q = null
ctrl.$parsers.push (viewValue) ->
ctrl.$setValidity 'unique-email', true
if ctrl.$valid
if viewValue?
$timeout.cancel(q) if q?
q = $timeout (->
ctrl.$setValidity 'checking-unique-email', false
$http.get("#{endpoint}/emails/#{viewValue}/exists").success (exists) ->
ctrl.$setValidity 'checking-unique-email', true
ctrl.$setValidity 'unique-email', exists is 'false'
q = null
) , attrs.checkDelay ? 1000
viewValue

Resources