angularjs directive validation unique email check before send - angularjs

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.

Related

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 can I add an async validator to an input field?

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 />

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

Custom validation not triggered when setting model value programmatically

Created a custom validation directive that invalidates an input when anything else than a number or space is input. When I change the value programmatically to something that should pass validation, the validation state is not changed.
Check this JSFIDDLE and see for yourself. Any ideas?
<div ng-app="test" ng-controller="Ctrl">
<form name="form">
<input custom-validation type="text" ng-model="box.text" name="text" />
</form>
<button ng-click="change()">Change to numbers only</button>
Why doesn't changing to numbers only pass the validation?
</div>
angular.module('test', []);
angular.module('test').directive('customValidation', function () {
'use strict';
return {
require: '?ngModel',
link: function (scope, element, attrs, ngModelCtrl) {
ngModelCtrl.$parsers.push(function removeIllegalCharacters(viewValue) {
if(viewValue === undefined){
ngModelCtrl.$setValidity('numbersAndSpacesOnly', true); //empty value is always valid
} else {
var clean = viewValue.replace(/[^0-9 ]+/g, '');
if (viewValue === clean) {
ngModelCtrl.$setValidity('numbersAndSpacesOnly', true);
} else {
ngModelCtrl.$setValidity('numbersAndSpacesOnly', false);
}
}
return viewValue;
});
}
};
});
angular.module('test').controller('Ctrl', function ($scope, $timeout) {
$scope.change = function () {
$scope.box.text = '12345';
}
});
ngModel uses 2 pipelines (arrays) of code for validation:
The $parsers array has functions to apply to the view value when it changes from the user; each function is called with the value returned form the previous, the first function is called with the view value and the return of the last function is written in the model. This is commonly used to validate and convert user input (e.g. from the text of an input type="text" to a number).
The $formatters array works similarly, but in the opposite direction. It receives the model value and transforms it, with the return of the last function being the new view value.
Functions in both pipelines may choose to call ngModel.$setValidity() to alter the validity state of the ngModel.
For the scope of this question: in order to validate the model value, you have to use the $formatters similarly to the $parsers you are already using:
angular.module('test').directive('customValidation', function () {
'use strict';
return {
require: '?ngModel',
link: function (scope, element, attrs, ngModelCtrl) {
function removeIllegalCharacters(value) {
if(value === undefined){
ngModelCtrl.$setValidity('numbersAndSpacesOnly', true); //empty value is always valid
} else {
var clean = value.replace(/[^0-9 ]+/g, '');
if (value === clean) {
ngModelCtrl.$setValidity('numbersAndSpacesOnly', true);
} else {
ngModelCtrl.$setValidity('numbersAndSpacesOnly', false);
}
}
return value;
}
ngModelCtrl.$parsers.push(removeIllegalCharacters);
ngModelCtrl.$formatters.push(removeIllegalCharacters);
}
};
});

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