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
Related
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);
}
};
In the documentation it shows this:
ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) {
var value = modelValue || viewValue;
// Lookup user by username
return $http.get('/api/users/' + value).
then(function resolved() {
//username exists, this means validation fails
return $q.reject('exists');
}, function rejected() {
//username does not exist, therefore this validation passes
return true;
});
};
But how do I connect this with an input field? It doesn't give an example of this.
You'd apply your validator inside of a directive link function. The directive must require ngModel, and then you'd apply that directive as an attribute to the input tag. In your case, that might look a bit like this:
angular.directive('isUniqueName', function () {
return {
require: 'ngModel',
link: function ($scope, $elem, $attrs, ngModel) {
ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) {
var value = modelValue || viewValue;
// Lookup user by username
return $http.get('/api/users/' + value).
then(function resolved() {
//username exists, this means validation fails
return $q.reject('exists');
}, function rejected() {
//username does not exist, therefore this validation passes
return true;
})
}
}
}
})
And then in your HTML code:
<input type='text' name='username' ng-model='username' is-unique-name />
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>
I have a directive for custom validation (verify a username doesn't already exist). The validation uses the $http service to ask the server if the username exists, so the return is a promise object. This is working fantastic for validation. The form is invalid and contains the myform.$error.usernameVerify when the username is already taken. However, user.username is always undefined, so it's breaking my ng-model directive. I think this is probably because the function in .success is creating it's own scope and the return value isn't used on the controllers $scope. How do I fix this so the ng-model binding still works?
commonModule.directive("usernameVerify", [
'userSvc', function(userSvc) {
return {
require: 'ngModel',
scope: false,
link: function(scope, element, attrs, ctrl) {
ctrl.$parsers.unshift(checkForAvailability);
ctrl.$formatters.unshift(checkForAvailability);
function checkForAvailability(value) {
if (value.length < 5) {
return value;
}
// the userSvc.userExists function is just a call to a rest api using $http
userSvc.userExists(value)
.success(function(alreadyUsed) {
var valid = alreadyUsed === 'false';
if (valid) {
ctrl.$setValidity('usernameVerify', true);
return value;
}
ctrl.$setValidity('usernameVerify', false);
return undefined;
});
}
}
}
}
]);
Here is my template:
<div class="form-group" ng-class="{'has-error': accountForm.username.$dirty && accountForm.username.$invalid}">
<label class=" col-md-3 control-label">Username:</label>
<div class="col-md-9">
<input name="username"
type="text"
class="form-control"
ng-model="user.username"
ng-disabled="user.id"
ng-minlength=5
username-verify
required />
<span class="field-validation-error" ng-show="accountForm.username.$dirty && accountForm.username.$error.required">Username is required.</span>
<span class="field-validation-error" ng-show="accountForm.username.$dirty && accountForm.username.$error.minlength">Username must be at least 5 characters.</span>
<span class="field-validation-error" ng-show="accountForm.username.$dirty && accountForm.username.$error.usernameVerify">Username already taken.</span>
</div>
</div>
Angular has a dedicated array of $asyncValidators for precisely this situation:
see https://docs.angularjs.org/api/ng/type/ngModel.NgModelController
ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) {
var value = modelValue || viewValue;
// Lookup user by username
return $http.get({url:'/api/users/' + value}).
then(function resolved() {
//username exists, this means validation fails
return $q.reject('exists');
}, function rejected() {
//username does not exist, therefore this validation passes
return true;
});
};
In order to get this to work, I needed to add "return value;" outside of the asynchronous call. Code below.
commonModule.directive("usernameVerify", [
'userSvc', function(userSvc) {
return {
require: 'ngModel',
scope: false,
link: function(scope, element, attrs, ctrl) {
ctrl.$parsers.unshift(checkForAvailability);
ctrl.$formatters.unshift(checkForAvailability);
function checkForAvailability(value) {
if (value.length < 5) {
return value;
}
userSvc.userExists(value)
.success(function(alreadyUsed) {
var valid = alreadyUsed === 'false';
if (valid) {
ctrl.$setValidity('usernameVerify', true);
return value;
}
ctrl.$setValidity('usernameVerify', false);
return undefined;
});
// Below is the added line of code.
return value;
}
}
}
}
]);
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.