Skip $asyncValidators until model is dirty - angularjs

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

Related

Displaying server message from $asyncValidators

I'm using AngularJS to send asynchronous validations. How do I display an error message fetched from server via $asyncValidators?
I have a validateName directive which uses $asyncValidators to validate user.name at the server. If the validation fails, the server responds with an errorMessage.
angular.module('myapp').directive('validateName', function($http, $q) {
return {
require: 'ngModel',
link: function(scope, element, attrs, ctrl) {
ctrl.$asyncValidators.name = function(modelValue, viewValue) {
if( ctrl.$dirty ) {
return $http.post('/validate-username', {username: viewValue}).then(function(response) {
if (!response.data.validUsername) {
return $q.reject(response.data.errorMessage);
}
return true;
});
} else {
return $q.resolve(true);
}
};
}
};
});
Below is my input field that uses the directive. I would like to display the errorMessage below that field.
<input name="name" class="form-control" ng-model="user.name" validate-name>
<!-- Here I would like to display the errorMessage somehow -->
Use a callback from your directive to send that message to your controller, then you can bind that message string to your DOM as you like
in your html add an attribute that will hold a reference to a function in your controller that will accept the error message from your directive and show it.
<input name="name" class="form-control" ng-model="user.name" get-error="showError" validate-name>
in your controller add the showError(message) function
$scope.showError = function(message) {
$scope.showMessage = message;
}
in your directive, you should call that callback function when you create the error message
// test if the var is actually a function reference before running it
if (angular.isFunction(scope.getError()) {
// this will run the function over at your controller
scope.getError()("This is error");
}
I ended up assigning the error message to the scope. Something like scope.form[attrs.name]['errorMessages'] = response.data.errors.

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

How do I attach validators on an input field in angular that accesses another model in the form

I have a range input (between [lowerValue] and [upperValue]) on a form and I want to make a reusable directive called 'validateGreaterThan' that can be attached to any form and use the ngModel $validators functionality so I can attach multiple ones onto an input.
You can check a simple demo on jsbin here:
http://jsbin.com/vidaqusaco/1/
I've set up a directive called nonNegativeInteger and that works correctly, however, the validateGreaterThan directive I have isn't working. How can I get it to reference the lowerValue?
I appreciate any help with this.
Here is the basic idea:-
Define 2 directives and let each directive refer to the other field buy passing its name. When the validator runs on the current field you can retrieve the model value of another field and now you have values from both fields and you can ensure the relation between the 2 fields.
As per my code below I have 2 fields minimumAmount and maximumAmount where the minimumAmount cannot be greater than the maximum amount and vice-versa.
<input name="minimumAmount" type="number" class="form-control"
ng-model="entity.minimumAmount"
less-than-other-field="maximumAmount" required/>
<input name="maximumAmount" type="number"
ng-model="entity.maximumAmount"
greater-than-other-field="minimumAmount"
class="form-control"/>
Here we have 2 directives lessThanOtherField and greaterThanOtherField and they both refer to other field as we pass the other field name. greater-than-other-field="minimumAmount" we are passing the other field.
.directive('lessThanOtherField', ['$timeout',function($timeout){
return {
require: 'ngModel',
link: function (scope, elm, attrs, ctrl) {
var xFieldValidatorName = 'lessThanOtherField';
var form = elm.parent().controller('form');
var otherFieldName = attrs[xFieldValidatorName];
var formFieldWatcher = scope.$watch(function(){
return form[otherFieldName];
}, function(){
formFieldWatcher();//destroy watcher
var otherFormField = form[otherFieldName];
var validatorFn = function (modelValue, viewValue) {
var otherFieldValue = otherFormField.hasOwnProperty('$viewValue') ? otherFormField.$viewValue : undefined;
if (angular.isUndefined(otherFieldValue)||otherFieldValue==="") {
return true;
}
if (+viewValue < +otherFieldValue) {
if (!otherFormField.$valid) {//trigger validity of other field
$timeout(function(){
otherFormField.$validate();
},100);//avoid infinite loop
}
return true;
} else {
// it is invalid, return undefined (no model update)
//ctrl.$setValidity('lessThanOtherField', false);
return false;
}
};
ctrl.$validators[xFieldValidatorName] = validatorFn;
});
}
};
}])
.directive('greaterThanOtherField', ['$timeout',function($timeout){
return {
require: 'ngModel',
link: function (scope, elm, attrs, ctrl) {
var xFieldValidatorName = 'greaterThanOtherField';
var form = elm.parent().controller('form');
var otherFieldName = attrs[xFieldValidatorName];
var formFieldWatcher = scope.$watch(function(){
return form[otherFieldName];
}, function(){
formFieldWatcher();//destroy watcher
var otherFormField = form[otherFieldName];
var validatorFn = function (modelValue, viewValue) {
var otherFieldValue = otherFormField.hasOwnProperty('$viewValue') ? otherFormField.$viewValue : undefined;
if (angular.isUndefined(otherFieldValue)||otherFieldValue==="") {
return true;
}
if (+viewValue > +otherFieldValue) {
if (!otherFormField.$valid) {//trigger validity of other field
$timeout(function(){
otherFormField.$validate();
},100);//avoid infinite loop
}
return true;
} else {
// it is invalid, return undefined (no model update)
//ctrl.$setValidity('lessThanOtherField', false);
return false;
}
};
ctrl.$validators[xFieldValidatorName] = validatorFn;
});
}
};
}])

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.

Resources