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.
Related
I'm trying to add an errors to my floating placeholder labels when certain conditions are met in my controller
However, I'm not sure the best way to go about this and my current implementing doesn't seem to be detecting the attribute change in the directive (custom-error stays set to "test").
Here's what I've got right now:
HTML:
<input type="password" float-placeholder
custom-error="test" placeholder="Confirm password"
required name="passwordSecond" id="passwordSecond"
ng-model="vs.PasswordSecond" />
Directive:
angular.module('myApp').directive('floatPlaceholder', function ($window) {
return {
restrict: 'A',
scope: {
customError: '#'
},
link: function (scope, element, attrs) {
element.after("<label class='floating-placeholder'>" + attrs.placeholder + "</label>");
var label = angular.element(ele.parent()[0].getElementsByClassName('floating-placeholder'));
element.on('blur', function() {
if (ele.val().length > 0) {
if (scope.customError) {
label.text(attrs.placeholder + ' - ' + scope.customError);
}
}
}
}
};
});
Controller:
angular.module('myApp').controller('SignupController', function factory() {
_this.confirmPassword = () => {
if(_this.PasswordFirst !== _this.PasswordSecond){
angular.element(signupForm.passwordSecond).attr('custom-error', _this.Error);
}
}
});
I'm using Angular 1.6
Validator Directive which Matches Passwords
To have a form match password inputs, create a custom directive that hooks into the ngModelController API ($validators):
app.directive("matchWith", function() {
return {
require: "ngModel",
link: postLink
};
function postLink(scope,elem,attrs,ngModel) {
ngModel.$validators.match = function(modelValue, viewValue) {
if (ngModel.$isEmpty(modelValue)) {
// consider empty models to be valid
return true;
}
var matchValue = scope.$eval(attrs.matchWith);
if (matchValue == modelValue) {
// it is valid
return true;
}
// it is invalid
return false;
};
}
})
For more information, see AngularJS Developer Guide - Forms - Modifying Built-in Validators
The DEMO
angular.module("app",[])
.directive("matchWith", function() {
return {
require: "ngModel",
link: postLink
};
function postLink(scope,elem,attrs,ngModel) {
ngModel.$validators.match = function(modelValue, viewValue) {
if (ngModel.$isEmpty(modelValue)) {
// consider empty models to be valid
return true;
}
var matchValue = scope.$eval(attrs.matchWith);
if (matchValue == modelValue) {
// it is valid
return true;
}
// it is invalid
return false;
};
}
})
<script src="//unpkg.com/angular/angular.js"></script>
<body ng-app="app">
<form name="form1">
<input type="password" name="password1" required
placeholder="Enter password"
ng-model="vm.password1" />
<br>
<input type="password" name="password2" required
placeholder="Confirm password"
ng-model="vm.password2"
match-with="vm.password1"
ng-model-options="{updateOn: 'blur'}" />
<br>
<p ng-show="form1.password2.$error.match">
Passwords don't match
</p>
<input type="submit" value="submit" />
</form>
</body>
Had a look at your code. Have you defined the scope variables in the SignUpController
_this.PasswordFirst and _this.PasswordSecond
Also this line in your controller
angular.element(signupForm.passwordSecond).attr('custom-error', _this.Error);
good suggestion would be to implement this in the directive as attributes can be accessed correctly in the directive
(I'm basing this on you saying 'custom-error stays set to "test"')
custom-error is looking for a variable of "test", not a string value of "test". Have you tried setting a variable test in your controller and updating that?
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);
}
};
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);
}
};
});
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;
}
}
}
}
]);
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