Prevent async unique validator to validate same value only when editing? - angularjs

I've got an AngularJS form that has a custom validator to check with the server backend whether the input value is unique or not. The unique validation is done by the mob-async-validate-unique directive in the example below.
The form looks somewhat like this:
<form class="form-floating" novalidate="novalidate" ng-submit="saveItem(item)">
<div class="form-group filled">
<label class="control-label">Key</label>
<input type="text" class="form-control" ng-model="item.Key" mob-async-validate-unique >
</div>
<div class="form-group">
<button type="submit" class="btn btn-lg btn-primary">Save</button>
</div>
</form>
I want to use the same form for both adding as well as editing the model that I put on $scope.
Everything works great except for the fact that the unique validator fires on the edit operation even when the value is the same as the original value and then validates unique as false because the value already exists. The validator marks the field invalid in both the following cases:
Change the field value and then edit it back to the original value
Submit the form without changing anything
What is the best way to achieve this? The naive way I can think of is that I'll have to store the original value on a $scope.originalValue variable, and then add an attribute on the unique validated element to name this variable. Within the validator I'll read this value from the $scope and compare it with the current value to make the validator accept it when the two values are the same. I'm going ahead and implementing this.
I use the unique validator in a generic way in several places (yes, there are several more attributes in use on the <input> element that I've not included in the code sample, for simplicity and legibility) and need the validator to function completely on it's own and ideally want to keep the controller $scope out of the picture so that I can use the custom async validator anyway / anywhere I want.

Depending on your used version of angularjs, there is absolutely no need for writing a custom async validator.
Angular has a built-in way of doing that. Check https://docs.angularjs.org/api/ng/type/ngModel.NgModelController
When you use the $asyncValidator as described in the docs, it does only validate against you API if all other validations succeed.
** EDIT **
Regarding your problem with async validation on editing an existing entry in your database, i suggest the following.
var originalData = {};
if(editMode) {
originalData = data.from.your.API;
}
$scope.formData = angular.copy(orignalData);
// in your async validator
if(value && value !== orginalData(key)) {
//do async validation
} else if(value == originalData(key)) {
return true; //field is valid
}

here is my directive to solve problem
validate if user press key
if model value same as initialValue. Async validation will not be applied
export default function ($http, API_URL, $q) {
"ngInject";
return {
require: 'ngModel',
restrict: 'A',
scope: {
asyncFieldValidator: '#',
initialValue: '#'
},
link: ($scope, element, attrs, ngModel) => {
const apiUrl = `${API_URL}${$scope.asyncFieldValidator}`;
element.on('keyup', e => {
ngModel.$asyncValidators.uniq = (modelValue, viewValue) => {
const userInput = modelValue || viewValue;
const checkInitial = $scope.initialValue === userInput;
return !checkInitial
? $http.get(apiUrl + userInput)
.then(res => res.status === 204 ? true : $q.reject())
: $q.resolve(`value is same`)
}
});
}
}
}

Related

Is there a better way to toggle Bootstrap form errors with Angular other than really long ng-class attributes?

I have a form that allows the user to change their password.
I have two directives I'm using to validate my form:
Does the password adhere to a specific format (8 char length, 1 letter, 1 number)
Do the new password boxes and the confirm password boxes match in input
The directives check for validity on the ngModel of the element and the set the validity so that I can see in the view with formName.modeName.$error.
I display an error message and toggle in the input boxes to have the has-error bootstrap class to change them red.
The problem with this is I end up with these extremely long ng-class attributes to determine whether or not to add has-error:
<div data-ng-class="{'has-error' : changePasswordForm.newPassword.$invalid && changePasswordForm.newPassword.$dirty || changePasswordForm.confirmPassword.$invalid && changePasswordForm.newPassword.$dirty && changePasswordForm.confirmPassword.$dirty}" class="form-group">
The example above is saying to add the has-error class if:
New password field is dirty and the format of the password is wrong
New password field and confirm password field are both dirty and the inputs don't match
Is this the Angular way? This makes the templates really hard to read for me. How should I be doing this?
Edit: In other words, should I be handling this logic in the template itself like I am doing, or should I be using toggle class in a directive or something similar?
You could add a directive to flag the has-error class, like this one (Bootstrap Form Validation Done Right in AngularJS):
angular.module('app', []).
directive('showErrors', function() {
return {
restrict: 'A',
link: function(scope, el) {
el.bind('blur', function() {
var valid = // is valid logic
el.toggleClass('has-error', valid);
});
}
}
});
ngMessages also has tools to solve this problem for the associated messages.
The best way I can think to accomplish this would be to write a custom directive for this:
<div class="form-group" validate-class="confirmPassword"> ... </div>
And in your directive
.directive('validateClass', [function() {
return {
restrict: 'A',
link: function validateClassLink(scope, element, attributes, ngForm) {
var fieldObj = ngForm[attributes.validateClass],
className = attributes.errorClass || 'has-error';
scope.$watch(function() {
return (fieldObj.$invalid && fieldObj.$dirty);
}, function(conditionMet) {
element.toggleClass(className, conditionMet);
});
},
require: '^form'
};
}]);
This is pretty rudimentary, but it should accomplish what you need.

How to use the last valid modelValue if a model becomes invalid?

I'm working on an application that saves changes automatically when the user changes something, for example the value of an input field. I have written a autosave directive that is added to all form fields that should trigger save events automatically.
template:
<input ng-model="fooCtrl.name" autosave>
<input ng-model="fooCtrl.email" autosave>
directive:
.directive('autosave', ['$parse', function ($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ngModel) {
function saveIfModelChanged () {
// save object containing name and email to server ...
}
ngModel.$viewChangeListeners.push(function () {
saveIfModelChanged();
});
}
};
}]);
So far, this all works fine for me. However, when I add validation into the mix, for example validating the input field to be a valid email address, the modelValue is set to undefined as soon as the viewValue is changed to an invalid email address.
What I would like to do is this: Remember the last valid modelValue and use this when autosaving. If the user changes the email address to be invalid, the object containing name and email should still be saved to the server. Using the current valid name and the last valid email.
I started out by saving the last valid modelValue like this:
template with validation added:
<input type="email" ng-model="fooCtrl.name" autosave required>
<input ng-model="fooCtrl.email" autosave required>
directive with saving lastModelValue:
.directive('autosave', ['$parse', function ($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ngModel) {
var lastModelValue;
function saveIfModelChanged () {
// remeber last valid modelValue
if (ngModel.$valid) {
lastModelValue = ngModel.$modelValue;
}
// save object containing current or last valid
// name and email to server ...
}
ngModel.$viewChangeListeners.push(function () {
saveIfModelChanged();
});
}
};
}]);
My question is, how to use lastModelValue while saving, but preserving the invalid value in the view?
EDIT:
Another possibility, as suggested by Jugnu below, would be wrapping and manipulating the build in validators.
I tried to following: wrap all existing validators and remember the last valid value, to restore it if validations fails:
Object.keys(ngModel.$validators).forEach(function(validatorName, index) {
var validator = ngModel.$validators[validatorName];
ngModel.$validators[validatorName] = createWrapper(validatorName, validator, ngModel);
});
function createWrapper(validatorName, validator, ngModel){
var lastValid;
return function (modelValue){
var result = validator(modelValue);
if(result) {
lastValid = modelValue;
}else{
// what to do here? maybe asign the value like this:
// $parse(attrs.ngModel).assign(scope, lastValid);
}
return result;
};
}
But I'm not sure how to continue with this approach either. Can I set the model value without AngularJS kicking in and try to validate that newly set value?
I have created a simple directive that serves as a wrapper on the ng-model directive and will keep always the latest valid model value. It's called valid-ng-model and should replace the usage of ng-model on places where you want to have the latest valid value.
I've created an example use case here, I hope you will like it. Any ideas for improvements are welcomed.
This is the implementation code for valid-ng-model directive.
app.directive('validNgModel', function ($compile) {
return {
terminal: true,
priority: 1000,
scope: {
validNgModel: '=validNgModel'
},
link: function link(scope, element, attrs) {
// NOTE: add ngModel directive with custom model defined on the isolate scope
scope.customNgModel = angular.copy(scope.validNgModel);
element.attr('ng-model', 'customNgModel');
element.removeAttr('valid-ng-model');
// NOTE: recompile the element without this directive
var compiledElement = $compile(element)(scope);
var ngModelCtrl = compiledElement.controller('ngModel');
// NOTE: Synchronizing (inner ngModel -> outside valid model)
scope.$watch('customNgModel', function (newModelValue) {
if (ngModelCtrl.$valid) {
scope.validNgModel = newModelValue;
}
});
// NOTE: Synchronizing (outside model -> inner ngModel)
scope.$watch('validNgModel', function (newOutsideModelValue) {
scope.customNgModel = newOutsideModelValue;
});
}
};
});
Edit: directive implementation without isolate scope: Plunker.
Since you are sending the entire object for each field modification, you have to keep the last valid state of that entire object somewhere. Use case I have in mind:
You have a valid object { name: 'Valid', email: 'Valid' }.
You change the name to invalid; the autosave directive placed at the name input knows its own last valid value, so the correct object gets sent.
You change the email to invalid too. The autosave directive placed at the email input knows its own last valid value but NOT that of name. If the last known good values are not centralized, an object like { name: 'inalid', email: 'Valid' } will be sent.
So the suggestion:
Keep a sanitized copy of the object you are editing. By sanitized I mean that any invalid initial values should be replaced by valid pristine ones (e.g. zeros, nulls etc). Expose that copy as a controller member, e.g. fooCtrl.lastKnowngood.
Let autosave know the last known good state, e.g. as:
<input ng-model="fooCtrl.email" autosave="fooCtrl.lastKnowngood" required />
Keep the last known good local value in that object; utilize the ng-model expression, e.g. as:
var lastKnownGoodExpr = $parse(attrs.autosave);
var modelExpr = $parse(attrs.ngModel);
function saveIfModelChanged () {
var lastKnownGood = lastKnownGoodExpr(scope);
if (ngModel.$valid) {
// trick here; explanation later
modelExpr.assign({fooCtrl: lastKnownGood}, ngModel.$modelValue);
}
// send the lastKnownGood object to the server!!!
}
Send the lastKnownGood object.
The trick, its shortcomings and how can it be improved: When setting the local model value to the lastKnownGood object you use a context object different than the current scope; this object assumes that the controller is called fooCtrl (see the line modelExpr.assign({fooCtrl: lastKnownGood}, ...)). If you want a more general directive, you may want to pass the root as a different attribute, e.g.:
<input ng-model="fooCtrl.email" autosave="fooCtrl.lastKnowngood" required
autosave-fake-root="fooCtrl" />
You may also do some parsing of the ng-model expression yourself to determine the first component, e.g. substring 0 → 1st occurence of the dot (again simplistic).
Another shortcoming is how you handle more complex paths (in the general case), e.g. fooCtrl.persons[13].address['home'].street - but that seems not to be your use case.
By the way, this:
ngModel.$viewChangeListeners.push(function () {
saveIfModelChanged();
});
can be simplified as:
ngModel.$viewChangeListeners.push(saveIfModelChanged);
Angular default validators will only assign value to model if its valid email address.To overcome that you will need to override default validators.
For more reference see : https://docs.angularjs.org/guide/forms#modifying-built-in-validators
You can create a directive that will assign invalide model value to some scope variable and then you can use it.
I have created a small demo for email validation but you can extend it to cover all other validator.
Here is fiddle : http://plnkr.co/edit/EwuyRI5uGlrGfyGxOibl?p=preview

Disabling form validation

Angularjs is running my forms through the FormController (eg tracking pristine, dirty, etc). I don't need this functionality; I'm sure it's adding overhead to my $digests.
How can I shut it off?
AFAIK there is no simple switch to turn off AngularJS validation. Actually most of the validation happens in the NgModelController and input directives - basically code in the input.js file. So, to get rid of the built-in validation you would have to re-develop code from this file (plus some others, like select).
Did you identify validation code as a performance bottleneck in your application?
UPDATE : This does NOT work ... well at least not in a way you'd like it to. Adding ng-non-bindable to the form or any input breaks ALL binding. So, your ng-model in the inputs won't work anymore. Sorry ....
ng-non-bindable is the solution to this problem.
It will prevent AngularJS from seeing the form as a directive. This will make AngularJS ignore the entire form:
<form name="inviteContactForm" ng-non-bindable>
This will make AngularJS ignore one part of a form:
<input type="email" name="email" ng-non-bindable>
You can read a bit about my whining on this issue here. http://calendee.com/preventing-angularjs-from-hijacking-forms/
Internally Angular creates factories out of directives by adding the Directive suffix to the directive name. So you can replace the validation and input directive factories with no-operational ones.
var noopDirective = function() { return function () {}; };
angular.module('myModule')
.factory('requiredDirective', noopDirective)
.factory('ngRequiredDirective', noopDirective)
.factory('inputDirective', noopDirective)
.factory('textareaDirective', noopDirective); // etc...
Similar to previous answer from #Chui Tey, having a directive that requires 'ngModel'. This is what I did
for disabling validations in runtime:
//disabling all validators
forEach(ngModelController.$validators, function(validator, validatorName){
ngModelController.$setValidity(validatorName, true);//mark invalid as valid
var originalValidator = ngModelController.$validators[validatorName]; //we save the original validator for being able to restore it in future
ngModelController.$validators[validatorName] = _.wrap(true);//overwrite the validator
ngModelController.$validators[validatorName].originalValidator = originalValidator;
});
//for restoring validations
forEach(ngModelController.$validators, function(validator, validatorName){
if(ngModelController.$validators[validatorName].originalValidator){
ngModelController.$validators[validatorName] = ngModelController.$validators[validatorName].originalValidator;
}
});
ngModelController.$validate(); //triger validations
A colleague suggested a nifty way of disabling validators. Here's an implementation:
<input type="radio" name="enableValidation" ng-model="$ctrl.validationEnabled" ng-value="true" />Enabled
<input type="radio" name="enableValidation" ng-model="$ctrl.validationEnabled" ng-value="false" />Disabled
<input type="number"
name="age"
ng-model="$ctrl.age"
min="20"
disable-validation="!$ctrl.validationEnabled" />
When disable-validation is true, then all validation rules automatically passes.
function disableValidation(scope, elem, attrs, ngModelController) {
function wrapOriginalValidators() {
var originalValidators = angular.copy(ngModelController.$validators);
Object.keys(originalValidators).forEach(function(key) {
ngModelController.$validators[key] = function(modelValue, viewValue) {
return scope.$eval(attrs.disableValidation) || originalValidators[key](modelValue, viewValue);
}
});
}
function watchDisableCriteria() {
scope.$watch(attrs.disableValidation, function() {
// trigger validation
var originalViewValue = ngModelController.$viewValue;
scope.$applyAsync(function() {
ngModelController.$setViewValue('');
});
scope.$applyAsync(function() {
ngModelController.$setViewValue(originalViewValue);
});
});
}
wrapOriginalValidators();
watchDisableCriteria();
}
angular.module('app', [])
.directive('disableValidation', function() {
return {
require: 'ngModel',
link: disableValidation
};
});
Obviously, you'd not use this for performance reasons, but when you
need to dynamically enable or disable validations.
Sample: https://plnkr.co/edit/EM1tGb

filters on ng-model in an input

I have a text input and I don't want to allow users to use spaces, and everything typed will be turned into lowercase.
I know I'm not allowed to use filters on ng-model eg.
ng-model='tags | lowercase | no_spaces'
I looked at creating my own directive but adding functions to $parsers and $formatters didn't update the input, only other elements that had ng-model on it.
How can I change the input of that I'm currently typing in?
I'm essentially trying to create the 'tags' feature that works just like the one here on StackOverflow.
I believe that the intention of AngularJS inputs and the ngModel direcive is that invalid input should never end up in the model. The model should always be valid. The problem with having invalid model is that we might have watchers that fire and take (inappropriate) actions based on invalid model.
As I see it, the proper solution here is to plug into the $parsers pipeline and make sure that invalid input doesn't make it into the model. I'm not sure how did you try to approach things or what exactly didn't work for you with $parsers but here is a simple directive that solves your problem (or at least my understanding of the problem):
app.directive('customValidation', function(){
return {
require: 'ngModel',
link: function(scope, element, attrs, modelCtrl) {
modelCtrl.$parsers.push(function (inputValue) {
var transformedInput = inputValue.toLowerCase().replace(/ /g, '');
if (transformedInput!=inputValue) {
modelCtrl.$setViewValue(transformedInput);
modelCtrl.$render();
}
return transformedInput;
});
}
};
});
As soon as the above directive is declared it can be used like so:
<input ng-model="sth" ng-trim="false" custom-validation>
As in solution proposed by #Valentyn Shybanov we need to use the ng-trim directive if we want to disallow spaces at the beginning / end of the input.
The advantage of this approach is 2-fold:
Invalid value is not propagated to the model
Using a directive it is easy to add this custom validation to any input without duplicating watchers over and over again
I would suggest to watch model value and update it upon chage: http://plnkr.co/edit/Mb0uRyIIv1eK8nTg3Qng?p=preview
The only interesting issue is with spaces: In AngularJS 1.0.3 ng-model on input automatically trims string, so it does not detect that model was changed if you add spaces at the end or at start (so spaces are not automatically removed by my code). But in 1.1.1 there is 'ng-trim' directive that allows to disable this functionality (commit). So I've decided to use 1.1.1 to achieve exact functionality you described in your question.
A solution to this problem could be to apply the filters on controller side :
$scope.tags = $filter('lowercase')($scope.tags);
Don't forget to declare $filter as dependency.
If you are using read only input field, you can use ng-value with filter.
for example:
ng-value="price | number:8"
Use a directive which adds to both the $formatters and $parsers collections to ensure that the transformation is performed in both directions.
See this other answer for more details including a link to jsfiddle.
I had a similar problem and used
ng-change="handler(objectInScope)"
in my handler I call a method of the objectInScope to modify itself correctly (coarse input). In the controller I have initiated somewhere that
$scope.objectInScope = myObject;
I know this doesn't use any fancy filters or watchers... but it's simple and works great. The only down-side to this is that the objectInScope is sent in the call to the handler...
If you are doing complex, async input validation it might be worth it to abstract ng-model up one level as part of a custom class with its own validation methods.
https://plnkr.co/edit/gUnUjs0qHQwkq2vPZlpO?p=preview
html
<div>
<label for="a">input a</label>
<input
ng-class="{'is-valid': vm.store.a.isValid == true, 'is-invalid': vm.store.a.isValid == false}"
ng-keyup="vm.store.a.validate(['isEmpty'])"
ng-model="vm.store.a.model"
placeholder="{{vm.store.a.isValid === false ? vm.store.a.warning : ''}}"
id="a" />
<label for="b">input b</label>
<input
ng-class="{'is-valid': vm.store.b.isValid == true, 'is-invalid': vm.store.b.isValid == false}"
ng-keyup="vm.store.b.validate(['isEmpty'])"
ng-model="vm.store.b.model"
placeholder="{{vm.store.b.isValid === false ? vm.store.b.warning : ''}}"
id="b" />
</div>
code
(function() {
const _ = window._;
angular
.module('app', [])
.directive('componentLayout', layout)
.controller('Layout', ['Validator', Layout])
.factory('Validator', function() { return Validator; });
/** Layout controller */
function Layout(Validator) {
this.store = {
a: new Validator({title: 'input a'}),
b: new Validator({title: 'input b'})
};
}
/** layout directive */
function layout() {
return {
restrict: 'EA',
templateUrl: 'layout.html',
controller: 'Layout',
controllerAs: 'vm',
bindToController: true
};
}
/** Validator factory */
function Validator(config) {
this.model = null;
this.isValid = null;
this.title = config.title;
}
Validator.prototype.isEmpty = function(checkName) {
return new Promise((resolve, reject) => {
if (/^\s+$/.test(this.model) || this.model.length === 0) {
this.isValid = false;
this.warning = `${this.title} cannot be empty`;
reject(_.merge(this, {test: checkName}));
}
else {
this.isValid = true;
resolve(_.merge(this, {test: checkName}));
}
});
};
/**
* #memberof Validator
* #param {array} checks - array of strings, must match defined Validator class methods
*/
Validator.prototype.validate = function(checks) {
Promise
.all(checks.map(check => this[check](check)))
.then(res => { console.log('pass', res) })
.catch(e => { console.log('fail', e) })
};
})();
You can try this
$scope.$watch('tags ',function(){
$scope.tags = $filter('lowercase')($scope.tags);
});
I came here to look for a solution that would actively change an input text and mask it with * for all but last 4 digits as we type. This is achieved by $formatters
eg: Account Num input box: 1234567890AHSB1 should display in the input box as **********AHSB
Answer is just a slight variation of that given by #pkozlowski.opensource above.
angular.module('myApp').directive('npiMask', function() {
return {
require: 'ngModel',
link: function($scope, element, attrs, modelCtrl) {
modelCtrl.$formatters.push(function(inputValue) {
var transformedInput = inputValue.toString().replace(/.(?=.{4,}$)/g, '*');
if (transformedInput !== inputValue) {
modelCtrl.$setViewValue(transformedInput);
modelCtrl.$render();
}
return transformedInput;
});
}
};
});
<input-text
name="accountNum"
label="{{'LOAN_REPAY.ADD_LOAN.ACCOUNT_NUM_LABEL' | translate}}"
ng-model="vm.model.formData.loanDetails.accountNum"
is-required="true"
maxlength="35"
size="4"
npi-mask>
</input-text>

How to add custom validation to an AngularJS form?

I have a form with input fields and validation setup by adding the required attributes and such. But for some fields I need to do some extra validation. How would I "tap in" to the validation that FormController controls?
Custom validation could be something like "if these 3 fields are filled in, then this field is required and needs to be formatted in a particular way".
There's a method in FormController.$setValidity but that doesn't look like a public API so I rather not use it. Creating a custom directive and using NgModelController looks like another option, but would basically require me to create a directive for each custom validation rule, which I do not want.
Actually, marking a field from the controller as invalid (while also keeping FormController in sync) might be the thing that I need in the simplest scenario to get the job done, but I don't know how to do that.
Edit: added information about ngMessages (>= 1.3.X) below.
Standard form validation messages (1.0.X and above)
Since this is one of the top results if you Google "Angular Form Validation", currently, I want to add another answer to this for anyone coming in from there.
There's a method in FormController.$setValidity but that doesn't look like a public API so I rather not use it.
It's "public", no worries. Use it. That's what it's for. If it weren't meant to be used, the Angular devs would have privatized it in a closure.
To do custom validation, if you don't want to use Angular-UI as the other answer suggested, you can simply roll your own validation directive.
app.directive('blacklist', function (){
return {
require: 'ngModel',
link: function(scope, elem, attr, ngModel) {
var blacklist = attr.blacklist.split(',');
//For DOM -> model validation
ngModel.$parsers.unshift(function(value) {
var valid = blacklist.indexOf(value) === -1;
ngModel.$setValidity('blacklist', valid);
return valid ? value : undefined;
});
//For model -> DOM validation
ngModel.$formatters.unshift(function(value) {
ngModel.$setValidity('blacklist', blacklist.indexOf(value) === -1);
return value;
});
}
};
});
And here's some example usage:
<form name="myForm" ng-submit="doSomething()">
<input type="text" name="fruitName" ng-model="data.fruitName" blacklist="coconuts,bananas,pears" required/>
<span ng-show="myForm.fruitName.$error.blacklist">
The phrase "{{data.fruitName}}" is blacklisted</span>
<span ng-show="myForm.fruitName.$error.required">required</span>
<button type="submit" ng-disabled="myForm.$invalid">Submit</button>
</form>
Note: in 1.2.X it's probably preferrable to substitute ng-if for ng-show above
Here is an obligatory plunker link
Also, I've written a few blog entries about just this subject that goes into a little more detail:
Angular Form Validation
Custom Validation Directives
Edit: using ngMessages in 1.3.X
You can now use the ngMessages module instead of ngShow to show your error messages. It will actually work with anything, it doesn't have to be an error message, but here's the basics:
Include <script src="angular-messages.js"></script>
Reference ngMessages in your module declaration:
var app = angular.module('myApp', ['ngMessages']);
Add the appropriate markup:
<form name="personForm">
<input type="email" name="email" ng-model="person.email" required/>
<div ng-messages="personForm.email.$error">
<div ng-message="required">required</div>
<div ng-message="email">invalid email</div>
</div>
</form>
In the above markup, ng-message="personForm.email.$error" basically specifies a context for the ng-message child directives. Then ng-message="required" and ng-message="email" specify properties on that context to watch. Most importantly, they also specify an order to check them in. The first one it finds in the list that is "truthy" wins, and it will show that message and none of the others.
And a plunker for the ngMessages example
Angular-UI's project includes a ui-validate directive, which will probably help you with this. It let's you specify a function to call to do the validation.
Have a look at the demo page: http://angular-ui.github.com/, search down to the Validate heading.
From the demo page:
<input ng-model="email" ui-validate='{blacklist : notBlackListed}'>
<span ng-show='form.email.$error.blacklist'>This e-mail is black-listed!</span>
then in your controller:
function ValidateCtrl($scope) {
$scope.blackList = ['bad#domain.example','verybad#domain.example'];
$scope.notBlackListed = function(value) {
return $scope.blackList.indexOf(value) === -1;
};
}
You can use ng-required for your validation scenario ("if these 3 fields are filled in, then this field is required":
<div ng-app>
<input type="text" ng-model="field1" placeholder="Field1">
<input type="text" ng-model="field2" placeholder="Field2">
<input type="text" ng-model="field3" placeholder="Field3">
<input type="text" ng-model="dependentField" placeholder="Custom validation"
ng-required="field1 && field2 && field3">
</div>
You can use Angular-Validator.
Example: using a function to validate a field
<input type = "text"
name = "firstName"
ng-model = "person.firstName"
validator = "myCustomValidationFunction(form.firstName)">
Then in your controller you would have something like
$scope.myCustomValidationFunction = function(firstName){
if ( firstName === "John") {
return true;
}
You can also do something like this:
<input type = "text"
name = "firstName"
ng-model = "person.firstName"
validator = "'!(field1 && field2 && field3)'"
invalid-message = "'This field is required'">
(where field1 field2, and field3 are scope variables. You might also want to check if the fields do not equal the empty string)
If the field does not pass the validator then the field will be marked as invalid and the user will not be able to submit the form.
For more use cases and examples see: https://github.com/turinggroup/angular-validator
Disclaimer: I am the author of Angular-Validator
I recently created a directive to allow for expression-based invalidation of angular form inputs. Any valid angular expression can be used, and it supports custom validation keys using object notation. Tested with angular v1.3.8
.directive('invalidIf', [function () {
return {
require: 'ngModel',
link: function (scope, elm, attrs, ctrl) {
var argsObject = scope.$eval(attrs.invalidIf);
if (!angular.isObject(argsObject)) {
argsObject = { invalidIf: attrs.invalidIf };
}
for (var validationKey in argsObject) {
scope.$watch(argsObject[validationKey], function (newVal) {
ctrl.$setValidity(validationKey, !newVal);
});
}
}
};
}]);
You can use it like this:
<input ng-model="foo" invalid-if="{fooIsGreaterThanBar: 'foo > bar',
fooEqualsSomeFuncResult: 'foo == someFuncResult()'}/>
Or by just passing in an expression (it will be given the default validationKey of "invalidIf")
<input ng-model="foo" invalid-if="foo > bar"/>
Here's a cool way to do custom wildcard expression validations in a form (from: Advanced form validation with AngularJS and filters):
<form novalidate="">
<input type="text" id="name" name="name" ng-model="newPerson.name"
ensure-expression="(persons | filter:{name: newPerson.name}:true).length !== 1">
<!-- or in your case:-->
<input type="text" id="fruitName" name="fruitName" ng-model="data.fruitName"
ensure-expression="(blacklist | filter:{fruitName: data.fruitName}:true).length !== 1">
</form>
app.directive('ensureExpression', ['$http', '$parse', function($http, $parse) {
return {
require: 'ngModel',
link: function(scope, ele, attrs, ngModelController) {
scope.$watch(attrs.ngModel, function(value) {
var booleanResult = $parse(attrs.ensureExpression)(scope);
ngModelController.$setValidity('expression', booleanResult);
});
}
};
}]);
jsFiddle demo (supports expression naming and multiple expressions)
It's similar to ui-validate, but you don't need a scope specific validation function (this works generically) and ofcourse you don't need ui.utils this way.
#synergetic I think #blesh suppose to put function validate as below
function validate(value) {
var valid = blacklist.indexOf(value) === -1;
ngModel.$setValidity('blacklist', valid);
return valid ? value : undefined;
}
ngModel.$formatters.unshift(validate);
ngModel.$parsers.unshift(validate);
Update:
Improved and simplified version of previous directive (one instead of two) with same functionality:
.directive('myTestExpression', ['$parse', function ($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ctrl) {
var expr = attrs.myTestExpression;
var watches = attrs.myTestExpressionWatch;
ctrl.$validators.mytestexpression = function (modelValue, viewValue) {
return expr == undefined || (angular.isString(expr) && expr.length < 1) || $parse(expr)(scope, { $model: modelValue, $view: viewValue }) === true;
};
if (angular.isString(watches)) {
angular.forEach(watches.split(",").filter(function (n) { return !!n; }), function (n) {
scope.$watch(n, function () {
ctrl.$validate();
});
});
}
}
};
}])
Example usage:
<input ng-model="price1"
my-test-expression="$model > 0"
my-test-expression-watch="price2,someOtherWatchedPrice" />
<input ng-model="price2"
my-test-expression="$model > 10"
my-test-expression-watch="price1"
required />
Result: Mutually dependent test expressions where validators are executed on change of other's directive model and current model.
Test expression has local $model variable which you should use to compare it to other variables.
Previously:
I've made an attempt to improve #Plantface code by adding extra directive. This extra directive very useful if our expression needs to be executed when changes are made in more than one ngModel variables.
.directive('ensureExpression', ['$parse', function($parse) {
return {
restrict: 'A',
require: 'ngModel',
controller: function () { },
scope: true,
link: function (scope, element, attrs, ngModelCtrl) {
scope.validate = function () {
var booleanResult = $parse(attrs.ensureExpression)(scope);
ngModelCtrl.$setValidity('expression', booleanResult);
};
scope.$watch(attrs.ngModel, function(value) {
scope.validate();
});
}
};
}])
.directive('ensureWatch', ['$parse', function ($parse) {
return {
restrict: 'A',
require: 'ensureExpression',
link: function (scope, element, attrs, ctrl) {
angular.forEach(attrs.ensureWatch.split(",").filter(function (n) { return !!n; }), function (n) {
scope.$watch(n, function () {
scope.validate();
});
});
}
};
}])
Example how to use it to make cross validated fields:
<input name="price1"
ng-model="price1"
ensure-expression="price1 > price2"
ensure-watch="price2" />
<input name="price2"
ng-model="price2"
ensure-expression="price2 > price3"
ensure-watch="price3" />
<input name="price3"
ng-model="price3"
ensure-expression="price3 > price1 && price3 > price2"
ensure-watch="price1,price2" />
ensure-expression is executed to validate model when ng-model or any of ensure-watch variables is changed.
Custom Validations that call a Server
Use the ngModelController $asyncValidators API which handles asynchronous validation, such as making an $http request to the backend. Functions added to the object must return a promise that must be resolved when valid or rejected when invalid. In-progress async validations are stored by key in ngModelController.$pending. For more information, see AngularJS Developer Guide - Forms (Custom Validation).
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;
});
};
For more information, see
ngModelController $asyncValidators API
AngularJS Developer Guide - Forms (Custom Validation).
Using the $validators API
The accepted answer uses the $parsers and $formatters pipelines to add a custom synchronous validator. AngularJS 1.3+ added a $validators API so there is no need to put validators in the $parsers and $formatters pipelines:
app.directive('blacklist', function (){
return {
require: 'ngModel',
link: function(scope, elem, attr, ngModel) {
ngModel.$validators.blacklist = function(modelValue, viewValue) {
var blacklist = attr.blacklist.split(',');
var value = modelValue || viewValue;
var valid = blacklist.indexOf(value) === -1;
return valid;
});
}
};
});
For more information, see AngularJS ngModelController API Reference - $validators.
In AngularJS the best place to define Custom Validation is Cutsom directive.
AngularJS provide a ngMessages module.
ngMessages is a directive that is designed to show and hide messages
based on the state of a key/value object that it listens on. The
directive itself complements error message reporting with the ngModel
$error object (which stores a key/value state of validation errors).
For custom form validation One should use ngMessages Modules with custom directive.Here i have a simple validation which will check if number length is less then 6 display an error on screen
<form name="myform" novalidate>
<table>
<tr>
<td><input name='test' type='text' required ng-model='test' custom-validation></td>
<td ng-messages="myform.test.$error"><span ng-message="invalidshrt">Too Short</span></td>
</tr>
</table>
</form>
Here is how to create custom validation directive
angular.module('myApp',['ngMessages']);
angular.module('myApp',['ngMessages']).directive('customValidation',function(){
return{
restrict:'A',
require: 'ngModel',
link:function (scope, element, attr, ctrl) {// 4th argument contain model information
function validationError(value) // you can use any function and parameter name
{
if (value.length > 6) // if model length is greater then 6 it is valide state
{
ctrl.$setValidity('invalidshrt',true);
}
else
{
ctrl.$setValidity('invalidshrt',false) //if less then 6 is invalide
}
return value; //return to display error
}
ctrl.$parsers.push(validationError); //parsers change how view values will be saved in the model
}
};
});
$setValidity is inbuilt function to set model state to valid/invalid
I extended #Ben Lesh's answer with an ability to specify whether the validation is case sensitive or not (default)
use:
<input type="text" name="fruitName" ng-model="data.fruitName" blacklist="Coconuts,Bananas,Pears" caseSensitive="true" required/>
code:
angular.module('crm.directives', []).
directive('blacklist', [
function () {
return {
restrict: 'A',
require: 'ngModel',
scope: {
'blacklist': '=',
},
link: function ($scope, $elem, $attrs, modelCtrl) {
var check = function (value) {
if (!$attrs.casesensitive) {
value = (value && value.toUpperCase) ? value.toUpperCase() : value;
$scope.blacklist = _.map($scope.blacklist, function (item) {
return (item.toUpperCase) ? item.toUpperCase() : item
})
}
return !_.isArray($scope.blacklist) || $scope.blacklist.indexOf(value) === -1;
}
//For DOM -> model validation
modelCtrl.$parsers.unshift(function (value) {
var valid = check(value);
modelCtrl.$setValidity('blacklist', valid);
return value;
});
//For model -> DOM validation
modelCtrl.$formatters.unshift(function (value) {
modelCtrl.$setValidity('blacklist', check(value));
return value;
});
}
};
}
]);
Some great examples and libs presented in this thread, but they didn't quite have what I was looking for. My approach: angular-validity -- a promise based validation lib for asynchronous validation, with optional Bootstrap styling baked-in.
An angular-validity solution for the OP's use case might look something like this:
<input type="text" name="field4" ng-model="field4"
validity="eval"
validity-eval="!(field1 && field2 && field3 && !field4)"
validity-message-eval="This field is required">
Here's a Fiddle, if you want to take it for a spin. The lib is available on GitHub, has detailed documentation, and plenty of live demos.

Resources