Validation in Angular forms – Part 1

I am going to write a few (two I think) posts on how to implement validation in Angular applications.  In this post I am going to talk about showing validation messages and control form submittals.  In the next post I will improve on the concept by showing how to help the mundane tasks of creating forms with some helpers in ASP.NET MVC.

Here are the goals I would like to achieve with my implementation.

  • Do not show errors for a control until user touches this control
  • Show errors as they occur during user input
  • Show errors for controls that the user did not touch when they click on submit button.
  • If all the data is valid, submit the form’s data to the server via ajax.

Let’s get started.  I am using ideas I learned by reading “Mastering Web Application Development with AngularJ” book in this post.  I improved (I hope) upon them by hiding the errors until the input field is “dirty” or when form is submitted.  I am using TypeScript here, but if you download the project, you fill find compiled JavaScript files there, so no fear if you are not using TypeScript.  First of all, I am going to write a “base” controller class that will help me show error messages.  That code is very simple, just returning true or false depending on whether or not a specific field is valid. This is an important point – this function (show error) is called for every field and for every validation directive (rule).

module app.core.controllers {

    export interface ICoreScope extends ng.IScope {
        showError?: (ngModelController: ng.INgModelController, error: string) => any;
    }

    export class CoreController {
        showError(ngModelController: ng.INgModelController, error: string) {
            if (ngModelController.$dirty) {
                return ngModelController.$error[error];
            }
            return false;
        }


        constructor($scope: ICoreScope) {
            $scope.showError = this.showError;
        }

    }


}

ngModelController is created by Angular for each input field on a form.  It is different from controller (my base class) that is tied to a view and exposes a model with properties.  Each property on such model is tied in turn to ngModelController.  Makes sense I hope.  What I call error (parameter) is going to be the name of my validation directive.  I am not going to use built-in Angular directives.  The reason for this will become clear in the next post.  So, let’s take a look at a couple of directives – required and maximum length.

    class RequiredDirective extends BaseDirective {
        constructor() {
            super();
            this.restrict = 'A';
            this.require = ['?ngModel'];
            var that = this;
            this.link = function (scope: app.core.controllers.ICoreScope, element: ng.IAugmentedJQuery, attributes: ng.IAttributes, controller: any) {

                var currentController: ng.INgModelController = app.directives.BaseDirective.getControllerFromParameterArray(controller);
                if (!currentController) return;
                var validator = function (value) {
                    if ((that.isEmpty(value) || value === false)) {
                        currentController.$setValidity('required', false);
                        return value;
                    } else {
                        currentController.$setValidity('required', true);
                        return value;
                    }
                };

                currentController.$formatters.push(validator);
                currentController.$parsers.unshift(validator);
            };
        }
    }

    class MaxLengthDirective extends BaseDirective {
        constructor() {
            super();
            this.restrict = 'A';
            this.require = ['?ngModel'];
            var that = this;
            this.link = function (scope: app.core.controllers.ICoreScope, element: ng.IAugmentedJQuery, attributes: ng.IAttributes, controller: any) {
                var maxLength: number = parseInt(element.attr(attributes.$attr['maxlen']));
                var currentController: ng.INgModelController = app.directives.BaseDirective.getControllerFromParameterArray(controller);
                if (!currentController) return;
                var validator = function (value) {
                    if (!that.isEmpty(value) && value.length > maxLength) {
                        currentController.$setValidity('maxlen', false);
                        return value;
                    } else {
                        currentController.$setValidity('maxlen', true);
                        return value;
                    }
                };

                currentController.$formatters.push(validator);
                currentController.$parsers.unshift(validator);
            };
        }
    }

Some helper methods, such as getControllerFromParametersArray, are not that important.  You can see that code in the project download.  If you look at the max length directive, as it is more complicated, you will see a few things.  First of all, I am using attr() method to get the actual value of the maximum length limit from the attribute that matches the directive.  Then I am pushing validate function into parsers and formatters – those are tried to each field controller.  One is invoked when pushing the data from UI into model, the other pushing the data from model into UI.  Then, I am simply comparing the length of the value in the input box to the limit from the parameter.  Easy, right?  When all said and done, here is the HMTL I would like to generate for my simple form.

<form name="contactForm" novalidate="novalidate" role="form">
    <div class="form-group">
        <label>Contact Type</label>
        <input class="form-control" data-maxlen="30" data-ng-model="model.Name" data-required="" name="Name" type="text">
        <div class="text-danger" ng-show="showError(contactForm.Name, 'maxlen')">
            Type name cannot be longer than 30.
        </div>
        <div class="text-danger" ng-show="showError(contactForm.Name, 'required')">
            Type is required
        </div>
    </div>
    <button class="btn btn-primary saveButton" data-form-submit="contactForm" data-form-submit-function="save()">Save</button>
    <button class="btn btn-warning cancelButton" data-ng-click="cancel()">Cancel</button>

</form>

As you see I am injecting code for both validation rules – required and max length and appropriate error messages.  This will work fine, but now how do we intercept the form submittal?  The attributes on Save button are used in yet another directive – form submittal directive I wrote, and it will greatly simplify the idea of validating the data before sending it to the server.  The reason I want to do this, is because I do not want to show any errors unless user changes some data for the field in question.  This simulates identical functionality in unobtrusive JavaScript validation that is uses by default in ASP.NET MVC apps.  I like this user experience more than showing the errors to being with.  Hence, there is dirty check inside showError function.  There is also another controller in Angular, form controller.  It contains the collection of field controllers I mentioned before.  Each form will have a form controller in the scope with the name of the form itself.  Each field controller also has a few properties, such as $pristine, $drity, $valid and $invalid.  The names explain well enough what those properties do.  And it also has a method to set the value of the corresponding field in the model – setViewValue.  We will take advantage of this functionality in our form submittal directive.  It will work with two attributes – one for form name, the other for the method to call on the controller to submit the form if it passes validation.  So, let’s take a look at that directive.

    class FormSubmitDirective extends BaseDirective {

        constructor(private utilities: app.core.services.IUtilities) {
            super();
            this.restrict = 'A';
            this.link = function (scope: ng.IScope, element: ng.IAugmentedJQuery, attributes: ng.IAttributes) {
                var $element = angular.element(element);

                $element.bind('click', function (eventObject: JQueryEventObject) {
                    eventObject.preventDefault();

                    var form: ng.IFormController = scope.$eval(attributes[FormSubmitDirectiveName]);

                    // trigger all pristine fields to be dirty and run validation
                    angular.forEach(form, function (fieldController: ng.INgModelController) {
                        if (fieldController.$pristine) {
                            fieldController.$setViewValue(fieldController.$viewValue);
                        }
                    });

                    if (form.$valid) {
                        // call function defined in another attribute
                        scope.$eval(attributes[FormSubmitDirectiveFunctionName]);
                    } else {
                        // apply scope changes to show error messages
                        scope.$apply();
                        utilities.showMessage('Please correct invalid values.');
                    }
                });
            };
        }
    }

 

You see above that when I link (activate) the directive, I am getting the element and bind to click event.  Of course he element is the submit button.  I am using preventDefault() to keep the form from actually being submitted by the browser, as I am going to send the data myself.  Then, I am getting the form controller reference by calling eval() function and passing it my attribute value, which is the form name.  Then I am looping through model controllers (remember one per field controllers) and re-setting field value to mark fields “dirty”, but only for those that are pristine, since dirty field controller’s validation state is always up to date.  Finally, I am checking overall form controller’s valid state, and if it passes, I am calling the function specified in the submit method name attribute. Finally, if the validation does not pass, I show a message to the user.  So, as a bonus, here is the TypeScript for the method to show message in my utilities class.  It is using Bootstrap of course.  It is very similar to please wait dialog I blogged about before.  So, here is your bonus code for my utilities class.  Really, simple implementation of MessageBox you are familiar with.  It does a bit more stuff, having support for additional buttons, more to come as I continue to blog on Angular.

 

/// <reference path="../../home/interfaces.ts" />
module app.core.services {


    export interface IUtilities {
        showPleaseWait: () => void;
        hidePleaseWait: () => void;
        showMessage: (content: string, buttons?: IButtonForMessage[]) => void;
    }

    export interface IButtonForMessage {
        mehtod?: () => void;
        label: string;
    }

    class Utilities implements IUtilities {
        showPleaseWait: () => void;
        hidePleaseWait: () => void;
        showMessage: (content: string) => void;

        constructor(private $window: ng.IWindowService, private globalsService: interfaces.IGLobals) {
            var that = this;
            var pleaseWaitDiv = angular.element(
                '<div class="modal" id="globalPleaseWaitDialog" data-backdrop="static" data-keyboard="false">' +
                '  <div class="modal-dialog">' +
                '    <div class="modal-content">' +
                '      <div class="modal-header">' +
                '         <h1>Processing...</h1>' +
                '      </div>' +
                '      <div class="modal-body" id="globallPleaseWaitDialogBody">' +
                '         <div class="progress progress-striped active">' +
                '           <div class="progress-bar" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">' +
                '           </div>' +
                '         </div>' +
                '        <div class="progress-bar progress-striped active"><div class="bar" style="width: 100%;"></div></div>' +
                '      </div>' +
                '    </div>' +
                '  </div>' +
                '</div>'
                );

            var messageDiv = angular.element(
                '<div class="modal" id="globalMessageDialog" tabindex="-1" role="dialog" data-backdrop="static" data-keyboard="true">' +
                '  <div class="modal-dialog">' +
                '    <div class="modal-content">' +
                '      <div class="modal-header">' +
                '        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>' +
                '        <h4 class="modal-title"></h4>' +
                '      </div>' +
                '      <div class="modal-body">' +
                '      </div>' +
                '      <div class="modal-footer">' +
                '       <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>' +
                '      </div>' +
                '    </div>' +
                '  </div>' +
                '</div>'
                );


            var resize = function (event: JQueryEventObject) {
                var dialog = angular.element('#' + event.data.name + ' .modal-dialog');
                dialog.css('margin-top', (angular.element(that.$window).height() - dialog.height()) / 2 - parseInt(dialog.css('padding-top')));
            };

            var animate = function (event: JQueryEventObject) {
                var dialog = angular.element('#' + event.data.name + ' .modal-dialog');
                dialog.css('margin-top', 0);
                dialog.animate({ 'margin-top': (angular.element(that.$window).height() - dialog.height()) / 2 - parseInt(dialog.css('padding-top')) }, 'slow');
                pleaseWaitDiv.off('shown.bs.modal', animate);

            };

            this.showPleaseWait = function () {
                angular.element($window).on('resize', { name: 'globalPleaseWaitDialog' }, resize);
                pleaseWaitDiv.on('shown.bs.modal', { name: 'globalPleaseWaitDialog' }, animate);
                pleaseWaitDiv.modal();
            };

            this.hidePleaseWait = function () {
                pleaseWaitDiv.modal('hide');
                angular.element($window).off('resize', resize);
            };


            this.showMessage = function (content: string, buttons?: IButtonForMessage[]) {
                angular.element($window).on('resize', { name: 'globalMessageDialog' }, resize);
                
                messageDiv.find('.modal-body').text(content);
                messageDiv.on('shown.bs.modal', { name: 'globalMessageDialog' }, animate);
                if (buttons) {
                    messageDiv.find('.modal-header').children().remove('button');
                    var footer = messageDiv.find('.modal-footer');
                    footer.empty();
                    angular.forEach(buttons, function(button: IButtonForMessage) {
                        var newButton = angular.element('<button type="button" class="btn"></button>');
                        newButton.text(button.label);
                        if (button.mehtod) {
                            newButton.click(function() {
                                messageDiv.modal('hide');
                                button.mehtod();
                            });    
                        } else {
                            newButton.click(function () {
                                messageDiv.modal('hide');
                            }); 
                        }
                        footer.append(newButton);
                    });
                    
                } else {
                    messageDiv.find('.modal-header').html('<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button><h4 class="modal-title"></h4>');
                    messageDiv.find('.modal-footer').html('<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>');
                }
                messageDiv.find('.modal-title').text(globalsService.applicatioName);
                messageDiv.modal();
            };
        }
    }

    angular.module('app.core.services.utilities', ['app.globalsModule'])
        .factory('utilities', ['$window', 'globalsService', function ($window: ng.IWindowService, globalsService: interfaces.IGLobals) {
            return new Utilities($window, globalsService);
        }]);
}

Here you go.  Full implementation of validation in angular through directives.  To extend this, just add more directives, everything else will just work.

You can download the project here.  Enjoy.

Thanks.

One Comment

  1. Pingback: Validation in Angular forms – Part 2 | Sergey Barskiy's Blog

Leave a Reply

Your email address will not be published. Required fields are marked *