Monday, December 3, 2012

Angular JS: Custom Validation via Directives

Okay, for this bit on Angular, I'm going to write up a quick bit on custom validation.  I've gone over form validation before, but I think that there are still plenty of cases that Angular's default validation just doesn't cover. A lot of people's first instinct is to resort to calling controller functions to do their validation. You could do that, but that would break the really slick validation model Angular already has in place.

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

25 comments:

  1. Nice article! Helped me a lot! Thanks!

    There is just a slight typo in the html markup:
    "regex-validated-flags" should be "regex-validate-flags" to match the directive(attr.regexValidateFlags).

    Regards

    ReplyDelete
  2. Ha! Thanks for catching that! I made the change.

    ReplyDelete
  3. I 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?

    ReplyDelete
  4. All 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.

    ReplyDelete
  5. Awesome, thanks for the update. I don't think you need this line however:

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

    ReplyDelete
    Replies
    1. 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.

      Thanks again.

      Delete
    2. 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!

      Delete
  6. Replies
    1. Sorry, 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.

      Delete
  7. Great 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)

    Right 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

    ReplyDelete
    Replies
    1. There's three things you could be talking about here, so let me cover all of them as best I can in a comment:

      1. 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.

      Delete
  8. 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.

    I 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!

    ReplyDelete
    Replies
    1. 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.

      If 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."

      Delete
  9. thanks for such a helpful article

    ReplyDelete
  10. excellent, saved my time
    for SSN validation use string like below.

    regex-validate="^\d\d\d-\d\d-\d\d\d\d$"

    Reddy

    ReplyDelete
    Replies
    1. 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.

      Delete
  11. Hi, 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).
    I 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!

    ReplyDelete
    Replies
    1. 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.

      Here is an example on Plunker.

      Just a really quick mock up. But it should serve the purpose.

      Delete
  12. Thanks, that was helpful

    ReplyDelete
  13. Hey, 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.

    ReplyDelete
  14. Hi. 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??

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

    &gtinput name="name" type="text" ng-model="account.name" required unique&lt

    ReplyDelete
    Replies
    1. If you want your validation to be fired "first", you'd use $formatters.push rather than $formatters.unshift. Same for $parsers.

      Delete
    2. Thanks so much that did the trick! Awesome post.

      Delete
  15. Here's my implementation: https://github.com/nelsonomuto/angular-ui-form-validation

    ReplyDelete

This form allows some basic HTML. It will only create links if you wrap the URL in an anchor tag (Sorry, it's the Blogger default)