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>
public class Invoice
{
...
[Required]
[MaxLength(50)]
[MinLength(5)]
[RegularExpression("C.*")]
public string CustomerName { get; set; }
...
}
[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 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);
}
});
}
};
}]);
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>
$scope.save = function () {
$http.post(appRoot + 'api/invoicesapi', $scope.item)
.success(function (data) {
$location.path('/');
}).error(function (data) {
$scope.modelState = data.ModelState;
});
};
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.
Nice blog Thank you for sharing Angularjs Online Training
ReplyDeleteI hope long life to your site. Above all, never be discouraged
ReplyDeletevrai voyance gratuite par mail