Validation in Angular forms – Part 2

I blogged recently about implementing validation in Angular forms.  In this post I will talk about the pattern I use in my applications, both Angular and pure ASP.NET MVC to supplement JavaScript based validation on the server.  First of, I would like to start by making a case why I like to generate my forms on the server, using ASP.NET MVC instead of just having pure HTML files as my route templates in Angular. There is really only one argument I believe that makes pure HTML better than Razor based templates (CSHTML files).  This reason is performance.  Now, let’s look at the reasons why Razor templates is better.  First of all, I have full IntelliSense while building those views by simply specifying the mode for the view.  As a result, I will make far fewer typos.  Also, I can handle localization much easier by injecting localized labels.  I also can use some code generation to inject some Angular (or unobtrusive JavaScript validation I blogged about much earlier).  And finally, I can mitigate the performance by writing a robot to pre-generate my views and save them as pure HTML files in advance of deployment.  Bottom line, I see no reason not to use CSHTML view in Angular applications.  Now that we established this fact – at least in my mind I did 🙂 – what do we do next?

What HTML do I want to produce?  Here is an example from Edit contact page I am using in this post. 

image

HTML for it looks as follows

<div>
    <h1>Edit Contact Type</h1>
</div>


<div>
    <form data-ng-cloak="" name="contactForm" novalidate="novalidate" role="form">
        <div class="form-group"><label>First Name</label>
            <input class="form-control" data-maxlen="50" data-ng-model="model.FirstName" data-required=""
                   name="FirstName" type="text"></input>

            <div class="text-danger" ng-show="showError(contactForm.FirstName, ‘maxlen’)">First name cannot be
                longer than 50.
            </div>
            <div class="text-danger" ng-show="showError(contactForm.FirstName, ‘required’)">First name is
                required
            </div>
        </div>
        <div class="form-group"><label>Last Name</label>
            <input class="form-control" data-maxlen="50" data-ng-model="model.LastName" data-required="" name="LastName"
                   type="text"></input>

            <div class="text-danger" ng-show="showError(contactForm.LastName, ‘maxlen’;)">Last name cannot be
                longer than 50.
            </div>
            <div class="text-danger" ng-show="showError(contactForm.LastName, ‘required’)">Last name is
                required
            </div>
        </div>
        <div class="form-group"><label>Contact Type</label>
            <select class="form-control" data-dropdown-Required="" data-ng-model="model.ContactTypeId"
                    name="ContactTypeId" ng-options="type.ContactTypeId as type.Name for type in contactTypes"
                    type="select"></select>

            <div class="text-danger" ng-show="showError(contactForm.ContactTypeId, ‘dropdownRequired’)">Contact
                type is required
            </div>
        </div>
        <div class="checkbox form-group"><label><input data-ng-model="model.IsActive" name="IsActive"
                                                       type="checkbox"></input>Active Contact</label>
        </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>
</div>

As you can see, I need to have my Angular validation code for all supported directives.  I have some labels as well as Bootstrap form formatting.  Quite a bit of repetitive HTML to write for all the forms in the system.  This is the reason why I want to improve on this experience in razor views, partial views to be exact.  Here is my plan for developing a solution.

  1. I need to create consistent repeatable way to validate fields on the server.  I am using Web Api, and I think validation attributes, such as RequiredAttribute in Data Annotations is the way to go.  Of course I need to expand on that with other custom attributes.
  2. I would like to extend these attributes with additional metadata to support my Angular validation, specifically to support generate of showError function calls
  3. I need to establish a patter for writing Angular validation directives.
  4. I need to develop HTML helpers to generate the entire form and input controls.

This is going to be a long post with a lot of code, get ready.

Let’s start with custom validation attributes.  These attributes need to accomplish two things for me.  They need to provide server side validation inside my Web Api controller’s methods.  They also need to leave a signal somewhere during view generation so that I can inject validation code blocks into HTML. I cannot inject HTML from attributes obviously, but what I can do is leave some markers in generate context somewhere.  I am going to use a less known feature of ASP.ENT MVC – IMetadataAware interface.  This interface has just one method that accepts model metadata instance that exists while CSHTML is being processed.  As a result, I can inject some metadata.  I am going to inject a name of the Angular validation directive as well as any additional HTML attributes that this directive needs.  I am going to wrap this information inside a helper class.  It holds mandatory directive name and error message fields.  If directive itself needs a value, such as max length directive, I am going to inject it into the directive attribute itself.  If not, I can leave attributeValue parameter blank.  Finally, any additional HTML attributes are injected via additionalAttributes parameter.  These attributes are intended to be used by a matching Angular validation directive.

    public class ValidationAttributeMetadata
    {

        public ValidationAttributeMetadata(string directiveName, string errorMessage, string attributeValue = "", Dictionary<string, string> additionalAttributes = null)
        {
            DirectiveName = directiveName;
            ErrorMessage = errorMessage;
            AttributeValue = attributeValue;
            AdditionalAttributes = additionalAttributes ?? new Dictionary<string, string>();
        }

        public string DirectiveName { get; set; }
        public string ErrorMessage { get; set; }
        public Dictionary<string, string> AdditionalAttributes { get; set; }
        public string AttributeValue { get; set; }

    }

 

 

Here is an example of MaxLengthAttribute I wrote that uses this class.

    public class CustomMaxLengthAttribute : MaxLengthAttribute, IMetadataAware
    {
        public CustomMaxLengthAttribute(int length)
            : base(length)
        {

        }
        public void OnMetadataCreated(ModelMetadata metadata)
        {
            var additionalMetadataValue = new ValidationAttributeMetadata(
                "maxlen", ErrorMessageString, Length.ToString(CultureInfo.InvariantCulture));
            metadata.AdditionalValues.Add(Guid.NewGuid().ToString(), additionalMetadataValue);
        }
    }

There you go.  Of course for each validation attribute I need to have matching Angular directive.  I am going to write my own instead of using the ones built into Angular so that I have more control over additional HTML attributes.  Here is what maximum length directive looks like in TypeScript.  If you download the project below, you will have access to JavaScript version as well.  If not, you can easily convert TS to JS.  Of you can convert it using TS playground.  Base directive class is very simple, and just lest me write less code.

    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);
            };
        }
    }
 
module app.directives {

    export class BaseDirective implements ng.IDirective {
        public priority: number;
        public template: string;
        public templateUrl: string;
        public replace: boolean;
        public transclude: any;
        public restrict: string;
        public scope: any;
        public link: (
        scope: ng.IScope,
        instanceElement: any,
        instanceAttributes: ng.IAttributes,
        controller: any
        ) => void;
        public compile: (
        templateElement: any,
        templateAttributes: ng.IAttributes,
        transclude: (scope: ng.IScope, cloneLinkingFn: Function) => void
        ) => any;
        public controller: (...injectables: any[]) => void;
        public isEmpty: (value: any) => boolean;
        public require: string[];
        constructor() {
            this.isEmpty = function (value) {
                return angular.isUndefined(value) || value === '' || value === null || value !== value;
            };
        }

        public static getControllerFromParameterArray: (controller: any) => ng.INgModelController = function (controller: any) {
            var currentController: ng.INgModelController;
            if (angular.isArray(controller) && controller.length > 0) {
                currentController = controller[0];
            } else {
                currentController = controller;
            }
            return currentController;
        };
    }

If we were to revisit my plan for this blog post, we will see the following.  I have established a way to write validation attributes used in .NET in such a way as to leave markers in the metadata context.  I also created matching Angular validation directive.  The only step left now is to glue it all together inside CSHTML views.  To do this, I am going to write a custom HTML input helper.  I am going to demonstrate one that creates textbox, but if you download the project it will also contain more code to handle also checkboxes and selects (dropdowns).  Typically, I would use TextBoxFor method.  I blogged about custom templates to support Knockout JS a long time ago.  In this post, I am going to do all the coding in C# instead of using external templates.  Primarily, I have no reason to adjust them after deployment, and I do not want to write a ton of Razor code in partial templates that will be compiled at run time by Razor parsing engine.  As a result, I am going to create custom HTML helper completely in C#.  It will of course look very much like standard helpers.  Instead of TextBoxFor and CheckboxFor I am going to write just one method – CustomInputFor that will take a parameter of input type.  The project includes a few types now, but I will eventually expand it to support more.

    public enum InputTagType
    {
        CheckBox,
        Text,
        Select
    }

So, here is the final signature of my method.

        public static MvcHtmlString CustomInputFor<TModel, TProperty>(
            this HtmlHelper<TModel> htmlHelper,
            Expression<Func<TModel, TProperty>> expression,
            InputTagType inputTagType = InputTagType.Text,
            string formName = null,
            IDictionary<string, object> htmlAttributes = null)
        {

The parameters in order are.

  1. Helper – this is just standard MVC HTML Helper class instance
  2. Expression is a pointer to a model property, such as @Html.CustomInputFor(m => m.ContactTypeId, …
  3. Type of input control to build.
  4. Form name that control belongs to.  You can leave this blank, and I will show you why in a minute.
  5. Any additional HTML attributes to inject into control as attributes.

So, why is form name optional?  Also, why do we even need it to begin with? If you look a bit earlier in the post, you will see that for each form Angular will create form controller based on form name, so I need this name in order to provide correct parameters to showError method.  Now, why is it optional?  The reason is that I am going to need a form name in a number of places, so I want to handle it transparently.  To do this I am going to replaces BeginForm with a custom one.  It will look as follows.

        public static CustomMvcForm BeginCustomForm(
            this HtmlHelper htmlHelper,
            string formName,
            IDictionary<string, object> htmlAttributes = null)
        {
            var tagBuilder = new TagBuilder("form");
            tagBuilder.MergeAttributes(htmlAttributes);
            tagBuilder.MergeAttribute("name", formName);
            tagBuilder.MergeAttribute("role", "form");
            tagBuilder.MergeAttribute("novalidate", "novalidate");
            tagBuilder.MergeAttribute("data-ng-cloak", "");
            htmlHelper.ViewContext.Writer.Write(tagBuilder.ToString(TagRenderMode.StartTag));
            var result = new CustomMvcForm(htmlHelper.ViewContext, formName);
            return result;
        }

As you can see I am passing in form name just once.  I create form tag and add a few extra attributes.  I have to turn off form validation so that I do not get duplicate validation in case I use required attributes that browsers know about.  I also do not want to mix validation built into the browser and the one I am going to be using.  I also use custom MVC form class.  I need this class to make the form name exposed through View Context.

    public class CustomMvcForm : IDisposable
    {
        public const string CurrentFormViewDataKey = "CurrentForm";
        private readonly ViewContext _viewContext;
        private bool _disposed;

        public CustomMvcForm(ViewContext viewContext, string formName)
        {
            if (viewContext == null)
            {
                throw new ArgumentNullException("viewContext");
            }
            if (string.IsNullOrEmpty(formName))
            {
                throw new ArgumentNullException("formName");
            }
            _viewContext = viewContext;
            viewContext.ViewData[CurrentFormViewDataKey] = formName;
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                _disposed = true;
                _viewContext.Writer.Write("</form>");
                _viewContext.FormContext = null;
            }
        }
    }

As you can see I am injecting form name into ViewContext based on a constant.  After than, I can always pull it back out when I am processing input creating methods.  To pull it out I can call the following

htmlHelper.ViewContext.ViewData[CustomMvcForm.CurrentFormViewDataKey].ToString());

It is slowly coming together.  Finally, the input helper itself

 

        public static MvcHtmlString CustomInputFor<TModel, TProperty>(
            this HtmlHelper<TModel> htmlHelper,
            Expression<Func<TModel, TProperty>> expression,
            InputTagType inputTagType = InputTagType.Text,
            string formName = null,
            IDictionary<string, object> htmlAttributes = null)
        {


            ModelMetadata modelMetadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);

            var groupBuilder = CreateGroupBuilder(inputTagType);

            var labelBuilder = CreateLabelBuilder(modelMetadata);

            var name = ExpressionHelper.GetExpressionText(expression);

            TagBuilder inputBuilder = CreateInputTagBuilder(modelMetadata, name, inputTagType, htmlAttributes);

            var stringBuilder = new StringBuilder();
            if (inputTagType != InputTagType.CheckBox)
            {
                stringBuilder.AppendLine(labelBuilder.ToString(TagRenderMode.Normal));
                stringBuilder.AppendLine(inputBuilder.ToString(TagRenderMode.Normal));
            }
            else
            {
                labelBuilder.InnerHtml = inputBuilder.ToString(TagRenderMode.Normal) + labelBuilder.InnerHtml;
                stringBuilder.AppendLine(labelBuilder.ToString(TagRenderMode.Normal));
            }

            AddValidationErrorMessageDivs(formName ?? htmlHelper.ViewContext.ViewData[CustomMvcForm.CurrentFormViewDataKey].ToString(), modelMetadata, name, stringBuilder);

            groupBuilder.InnerHtml = stringBuilder.ToString();
            return new MvcHtmlString(groupBuilder.ToString(TagRenderMode.Normal));

        }

Let’s walk through this code.  First of all, I need to build a div for Bootstrap control support.  This is the call to CreateGroupBuilder.

        private static TagBuilder CreateGroupBuilder(InputTagType inputTagType)
        {
            var groupBuilder = new TagBuilder("div");
            groupBuilder.AddCssClass("form-group");
            if (inputTagType == InputTagType.CheckBox)
            {
                groupBuilder.AddCssClass("checkbox");
            }
            return groupBuilder;
        }

The code is slightly different for checkbox, also driven by Twitter Bootstrap guidelines.  After that I cam create label, using CreateLabelBuilder method.  In this method again I am using built in display name attribute.  If it is missing, I am using property name itself.

        private static TagBuilder CreateLabelBuilder(ModelMetadata modelMetadata)
        {
            var labelBuilder = new TagBuilder("label");
            labelBuilder.SetInnerText(modelMetadata.DisplayName ?? modelMetadata.PropertyName);
            return labelBuilder;
        }

Inside the CreateInputTagBuilder I am creating the actual input tag.

        private static TagBuilder CreateInputTagBuilder(
            ModelMetadata modelMetadata,
            string fullHtmlFieldName,
            InputTagType inputTagType,
            IDictionary<string, object> htmlAttributes)
        {
            TagBuilder inputBuilder;
            if (inputTagType == InputTagType.Select)
            {
                inputBuilder = new TagBuilder("select");
            }
            else
            {
                inputBuilder = new TagBuilder("input");
            }
            if (htmlAttributes != null)
            {
                inputBuilder.MergeAttributes(htmlAttributes);
            }
            inputBuilder.MergeAttribute("type", GetInputTypeString(inputTagType));
            inputBuilder.MergeAttribute("name", fullHtmlFieldName, true);
            if (inputTagType != InputTagType.CheckBox)
            {
                inputBuilder.AddCssClass("form-control");
            }
            inputBuilder.MergeAttribute("data-ng-model", string.Format("model.{0}", fullHtmlFieldName), true);

            foreach (var additionalValue in modelMetadata.AdditionalValues.Values)
            {
                var attribute = additionalValue as ValidationAttributeMetadata;
                if (attribute != null)
                {
                    inputBuilder.MergeAttribute(NormalizeDirectiveName(attribute.DirectiveName), attribute.AttributeValue, true);
                    if (attribute.AdditionalAttributes != null)
                    {
                        foreach (var key in attribute.AdditionalAttributes.Keys)
                        {
                            inputBuilder.MergeAttribute(key, attribute.AdditionalAttributes[key]);
                        }
                    }
                }

            }
            return inputBuilder;
        }

In the code above I first figure out what tag I need to build, input or select.  I add type of input attribute next.  Then I ma handling slight HTML differences for checkbox.  Then I am generaring binding attribute for Angular – data-ng-model.  As you can see I am binding to “model” property inside the controller.  This is a convention I picked, putting model into a property instead of adding properties directly to controller.  This makes JavaScript code simpler, I can just add this.model to the server calls.  Then I am running through attributes I injected via IMetadataAware interface and adding them as HTML attributes for my Angular directives if necessary.  If you jump back to CustomInputFor, you will see that I am putting those tags together forming the HTML in the beginning of the post.  Finally, I am running through the same additional attributes again, this time injecting calls to showError with appropriate error messages.

Let’s take a look at the Contact class now that is using my validation attributes.

    public class ContactType : EditableModel
    {
        public int ContactTypeId { get; set; }

        [CustomRequired(ErrorMessage = "Type is required")]
        [CustomMaxLength(30, ErrorMessage = "Type name cannot be longer than 30.")]
        [Display(Name = "Contact Type")]
        public string Name { get; set; }

    }

As you can see I am using Max Length and Required attributes.  I am also setting label name through Display attribute.  I am not localizing any of them, but they support localization through ErrorMessageResourceType and ErrorMessageResourceName properties. There you have it.  Now if compare the HTML I posted in the beginning of the post to the code in the Contact Type edit form:

@using AngularAspNetMvc.Web.Core.HtmlHelpers
@model AngularAspNetMvc.Models.Contacts.ContactType
<div>
    <h1>Edit Contact Type</h1>
</div>


<div>
    @using (@Html.BeginCustomForm("contactForm"))
    {
        @Html.CustomInputFor(m => m.Name)
        @Html.CustomSaveButton()
        @Html.CustomCancelButton()
    }
</div>

you will see that the CSHTML approach is much cleaner, much shorter and takes advantage of server aside validation code I have to write anyway.  Win-win if you ask me.

You can download the project here.

Let me know what you think.

Thanks.

One Comment

  1. Pingback: Generic MVC View Controller for Angular Apps | Sergey Barskiy's Blog

Leave a Reply

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