Monday, September 9, 2013

Hooking AngularJS validation to ASP.NET Web API Validation

This is a work in progress, but something I wanted to get out there after a hair-pulling time toying with it.  AngularJS is fast becoming my new favorite client-side javascript framework.  It’s limited in the dependencies that it needs, and in the code it makes you use in your model.  That said, it’s fairly opinionated about, well everything.  Including validation.  When you stay in their paradigm, it’s pretty sweet:

<input type="email" ng-model="user.email" name="uEmail" required/><br />
  <div ng-show="form.uEmail.$dirty && form.uEmail.$invalid">Invalid:
    <span ng-show="form.uEmail.$error.required">Tell us your email.</span>
    <span ng-show="form.uEmail.$error.email">This is not a valid email.</span>
  </div>

But I’d like to be able to reuse server-side validation as well.  In C#, one way of doing server side validation is to use attributes:

public class Invoice
  {
...
      [Required]
      [MaxLength(50)]
      [MinLength(5)]
      [RegularExpression("C.*")]
      public string CustomerName { get; set; }
...
}

In this silly example, CustomerName must be between 5 and 50 characters and start with the letter C.  In WebAPI, ASP.NET will push validation errors into ModelState automatically:

[HttpPost]
public virtual HttpResponseMessage Post(Invoice item)
{
    if (ModelState.IsValid)
    {
        var db = GetDbContext();
        item = db.Invoices.Add(item);
        db.SaveChanges();
        return Request.CreateResponse(HttpStatusCode.OK, item); 
    }
    return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
}

The end result is that after a post, the server will return _either_ JSON of the submitted item OR a 400 error with the validation errors.

The problem is, Angular, being server-agnostic, doesn’t have any way to wire up this ModelState to the validation framework.

Gluing it all together

The solution I’ve come up with so far is to use a custom directive to bridge ASP.NET’s ModelState and AngularJS’s validation.

app.directive('serverValidate', ['$http', function ($http) {
    return {
        require: 'ngModel',
        link: function (scope, ele, attrs, c) {
            console.log('wiring up ' + attrs.ngModel + ' to controller ' + c.$name);              
            scope.$watch('modelState', function() {
                if (scope.modelState == null) return;
                var modelStateKey = attrs.serverValidate || attrs.ngModel;
                modelStateKey = modelStateKey.replace('$index', scope.$index);
                console.log('validation for ' + modelStateKey);
                if (scope.modelState[modelStateKey]) {                            
                    c.$setValidity('server', false);
                    c.$error.server = scope.modelState[modelStateKey];
                } else {
                    c.$setValidity('server', true);                            
                }                        
            });                  
        }
    };
}]);

This code watches a variable called ‘modelState’ for changes.  When it changes, it then wires any errors found to the appropriate Angular validation $error object.

Here is an example of binding it:

<div class="form-group" data-ng-class="{'has-error':!form.CustomerName.$valid}">
    <label>Customer</label>
    <input name="CustomerName" type="text" placeholder="Customer" class="form-control" data-ng-model="item.CustomerName" data-server-validate />
    <div class="has-error" data-ng-repeat="errorMessage in form.CustomerName.$error.server">{{errorMessage}}</div>
</div>

And an example method where we call the WebAPI example above:

$scope.save = function () {
   $http.post(appRoot + 'api/invoicesapi', $scope.item)
       .success(function (data) {
           $location.path('/');
       }).error(function (data) {
           $scope.modelState = data.ModelState;
       });
};

The end result is that on post, if the server doesn’t think the model is valid, it returns ModelState, which our data-server-validate directive wires up to Angular’s validation.

Devil in the Details

There is some nuance that I’ve glossed over though, and it’s time to fess up.  It all centers around the fact that ASP.NET ModelState has one naming scheme, while Angular has another for binding, and yet another for validation.  I hide this above by naming the server parameter ‘item’. Let me explain with an example.  Imagine a server method like this:

public HttpResponseMessage Post(Invoice entity)

In this case, ModelState will look like this:

entity.CustomerName”:[“Some error1”]

If you are data binding to a variable named item, then your scope looks like this:

$scope.item = {CustomerName:”bob”}

and Angular’s binding will look like this:

<input name="CustomerName" type="text" data-ng-model="item.CustomerName"/>

But Angular’s validation is on elements, not model, so validation has yet another naming scheme:

<div class="has-error" data-ng-repeat="errorMessage in form.CustomerName.$error.server">{{errorMessage}}</div>

So, in many cases the fix is simple:  Just keep the client and server names the same, and keep up with the element’s name (you _could_ name the form “item”, but that could get confusing, for reasons we’ll see next).  In my case, I just call it ‘item’ both at the server and client, though I could just as well have called the client-side “entity”.

BUT in the case of nested collections, it gets trickier.  Here, ModelState includes an index:

“item.LineItems[0].Description”:[“Some error”]

While Angular typically lets you do something like this:

<li data-ng-repeat=”lineItem in item.LineItems”
<input type="text" data-ng-model="lineItem.Description" />
</li>

In this case,  we have to tell the directive which ModelState key to associate with this control, and specify a “sub-form” context that Angular will use when validating:

<li data-ng-repeat=”lineItem in item.LineItems” ng-form="lineItemsForm">
<input type="text" data-ng-model="lineItem.Description" data-server-validate=”item.LineItems[$index].Description” />
<div data-ng-repeat="errorMessage in lineItemsForm.Description.$error.server">{{errorMessage}}</div>
</li>

Alternatively, we can be sure the binding syntax matches ModelState:

<input type="text" data-ng-model="item.LineItems[$index].Description" data-server-validate />

The directive will pull either one, replace $index, and use it for the key that it uses to lookup errors in modelstate.