What you really want to do is build a directive that requires ngModel. Requiring ngModel will pass the ngModelController into the linking function as the fourth argument. The ngModel controller has a lot of handy functions on it, in particular there is $setValidity, which allows you to set the state of a model field has $valid or $invalid as well as set an $error flag.
Here is an example of a simple regex validation with a custom validation directive in JSFiddle:
Here is the code for the custom validation directive
app.directive('regexValidate', function() { return { // restrict to an attribute type. restrict: 'A', // element must have ng-model attribute. require: 'ngModel', // scope = the parent scope // elem = the element the directive is on // attr = a dictionary of attributes on the element // ctrl = the controller for ngModel. link: function(scope, elem, attr, ctrl) { //get the regex flags from the regex-validate-flags="" attribute (optional) var flags = attr.regexValidateFlags || ''; // create the regex obj. var regex = new RegExp(attr.regexValidate, flags); // add a parser that will process each time the value is // parsed into the model when the user updates it. ctrl.$parsers.unshift(function(value) { // test and set the validity after update. var valid = regex.test(value); ctrl.$setValidity('regexValidate', valid); // if it's valid, return the value to the model, // otherwise return undefined. return valid ? value : undefined; }); // add a formatter that will process each time the value // is updated on the DOM element. ctrl.$formatters.unshift(function(value) { // validate. ctrl.$setValidity('regexValidate', regex.test(value)); // return the value or nothing will be written to the DOM. return value; }); } }; });
... and here is an implementation of our custom validation directive in markup:
<div ng-app="myApp"> <div ng-controller="MainCtrl"> <form name="myForm" ng-submit="doSomething()"> <label>Must contain the word <strong>blah</strong> (case insensitive) <br/> <!-- set up the input, make sure it: 1. has ng-model 2. has a name="" so we can reference it in the model. 3. has regex-validate 4. (optional) has regex-validate-flags --> <input type="text" placeholder="Enter text here" ng-model="test" name="test" regex-validate="\bblah\b" regex-validate-flags="i"/> </label> <!-- set up some sort of output for validation the format here is: [formName].[fieldName].$error.[validationName] where validation name is determined by ctrl.$setValidity(validationName, true/false) in your custom directive. --> <span style="color:red" ng-show="myForm.test.$error.regexValidate">WRONG!</span> <div> <!-- for added measure, disable the submit if the form is $invalid --> <button type="submit" ng-disabled="myForm.$invalid">Submit</button> </form> </div> </div>
After all is said and done, we have a reusable validation that we can now seamlessly wire up with no additional function calls that integrates into Angular's already awesome validation. Within the custom directive itself, there are thousands of ways to skin the cat I skinned above, the possibilities are really up to the author. As long as we stick with something maintainable and testable, I think any solution anyone comes up with is perfect. I'm sure there are probably a few good Github repositories out there for angular validation directives, but it's quick and easy to make your own too. Have fun.
EDIT: I've written an entry on validation inside an ng-repeat: Validating Form Elements in a Repeat
Nice article! Helped me a lot! Thanks!
ReplyDeleteThere is just a slight typo in the html markup:
"regex-validated-flags" should be "regex-validate-flags" to match the directive(attr.regexValidateFlags).
Regards
Ha! Thanks for catching that! I made the change.
ReplyDeleteI noticed in your example if $scope.test is set to blah the form still shows as invalid -- appears to be because ctrl.$viewValue is NaN when it runs initally?
ReplyDeleteAll too true. Thank you for reminding me to get back to this. Since I wrote this I learned I also needed to set up a $formatter as well as a $parser. I've made the change and I'll add additional explanation to the post.
ReplyDeleteAwesome, thanks for the update. I don't think you need this line however:
ReplyDelete//test and set the INITIAL validity for regexValidate.
ctrl.$setValidity('regexValidate', regex.test(ctrl.$viewValue));
From my tests, ctrl.$viewValue is always NaN at that point.
Yep, another oversight. Serves me right for posting a blog entry right after I just figured out how to do this. Then again, part of the reason I do this is it's just not really well documented yet and I wanted to give others a hand.
DeleteThanks again.
Appreciate your post -- I'd not seen any examples that included the $formatters in a validation directive so would have been stuck for a while had I not found this!
DeleteWhy not ng-pattern?
ReplyDeleteSorry, I missed this comment. I realize ng-pattern is there. I guess I just wanted an example of some sort of custom validator, which was really the point of the blog entry. I was struggling to come up with a validator that wasn't already covered by the framework.
DeleteGreat stuff, thanks for writing this. One question though, and I'm sure I'm just missing something obvious, but how do you get Angular to pop-up the nice "This field is required" message with a custom validator? (or whatever wording you want)
ReplyDeleteRight now I've written a simple directive that validates a text input field, and it correctly sets "ng-invalid" on the element when the validator fails, but it still allows me to submit the form. Thanks a bunch for any help!!
-Graff VK
There's three things you could be talking about here, so let me cover all of them as best I can in a comment:
Delete1. you can explicitly hide or show DOM elements with something like <span ng-show="formName.fieldName.$error.required">this field is required</span>
2. just putting the HTML5 required attribute on an input will tell newer browsers, such as the latest version of Chrome, to popup a little bubble near the the input saying a value is required when the user submits the invalid form.
3. Using the HTML5 placeholder="Enter text here" attribute will put some dummy text in the textbox to let the user know that they should enter something in the box.
I hope one of those answers your question.
Thanks, let me try and re-phrase my question a bit more clearly. What I like about the built-in "required" validation directive is if you attempt to submit the form without inputting anything in the field, 2 things happen: You get a nice little pop-up telling you it's required, AND the form won't submit.
ReplyDeleteI was (naively?) hoping that a custom validator would do the same thing - or at least prevent form submission. But as I stated before, I can still submit the form even when my custom validator is "failing". (setting validity to false on the model)
The pop-up bubble is probably the least important thing because - as you demonstrated - I can always show/hide a span tag with some wording next to the field to indicate why it's not valid. But I was hoping that by setting validity to false on the model, Angular would know that the form cannot be submitted. So either I'm wrong in that assumption OR I'm doing something wrong in my code.
I appreciate your feedback, thanks for any additional tips or info you might have!
Well there is one quirk with validation in Angular I'm not sure I've mentioned elsewhere in my blog. If you have a form with only one input/select element in it, it will submit even if it's invalid.
DeleteIf you look in the angular source, this is done by design. And according to documentation, this is being done to conform with the HTML spec.
Check out here: http://docs.angularjs.org/api/ng.directive:form
Look for: "Submitting form and preventing default action."
thanks for such a helpful article
ReplyDeleteexcellent, saved my time
ReplyDeletefor SSN validation use string like below.
regex-validate="^\d\d\d-\d\d-\d\d\d\d$"
Reddy
Just so you know, Angular will do regex validation by default with ng-pattern. The example above was something I threw together *just* to make an example.
DeleteHi, thanks a lot for the sample code and for explaining how all this works. I have a question. I'm dealing with a standard "change your password" form where you need to validate that the password field contains the same value as another form field (let's say a "repeat your password" or password 2 field).
ReplyDeleteI want that if both fields are not equal between each other call the $setValidity on both ngmodels but I'm not finding the way to call from one ctrl.$parsers.unshift or directive link function the $setValidity or the other field I'm not currently validating. Do you know how can I get that? I'm really lost..
Thanks a lot!
Sure, it's pretty simple, you'd follow the same principles, but you'd probably want to pass the other property from the scope you'd want to check. And you might want to add a watch to that property as well.
DeleteHere is an example on Plunker.
Just a really quick mock up. But it should serve the purpose.
Thanks, that was helpful
ReplyDeleteGreat post!
ReplyDeleteHey, this was super helpful! Of all the tutorials that have saved my sanity lately, this is exceptionally well written. I appreciate the comments along with the code. Those will be useful for a long time.
ReplyDeleteHi. Thanks for the above it was helpful. I followed the code above as a template to write a unique directive. However, when used in conjunction with required, required is set to invalid every time my custom unique check is set to false. I assume it is something stupid I'm doing??
ReplyDeleteAny help would be appreciated.
companyProfileModule.directive('unique', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elem, attr, ngModel) {
// add a parser that will process each time the value is
// parsed into the model when the user updates it.
ngModel.$parsers.unshift(function(value) {
// test and set the validity after update.
var valid = !(value == 'test' && scope.account.id != 2);
ngModel.$setValidity('unique', valid);
// if it's valid, return the value to the model,
// otherwise return undefined.
return valid ? value : undefined;
});
// add a formatter that will process each time the value
// is updated on the DOM element.
ngModel.$formatters.unshift(function(value) {
// test and set the validity after update.
var valid = !(value == 'test' && scope.account.id != 2);
ngModel.$setValidity('unique', valid);
// return the value or nothing will be written to the DOM.
return value;
});
}
};
});
>input name="name" type="text" ng-model="account.name" required unique<
If you want your validation to be fired "first", you'd use $formatters.push rather than $formatters.unshift. Same for $parsers.
DeleteThanks so much that did the trick! Awesome post.
DeleteHere's my implementation: https://github.com/nelsonomuto/angular-ui-form-validation
ReplyDeleteThanks for this post. It helped me A LOT!
ReplyDeleteLine "return valid ? value : undefined;" causes problems with multiple validators. For example you are checking if any number entered and also has required validator (any chars entered) and the requirement is to show required only when there are no characters entered. Returning undefined causes $error required to appear falsely.
ReplyDelete