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.
7 comments:
Great start Daniel. Have you run into the issue that once a field is invalidated from the server it stays that way? I'm thinking that once the user blurs the input it should reset the invalid flag.
Added this below the scope.$watch:
ele.bind('focus', function (evt) {
c.$setValidity('server', true);
}).bind('blur', function (evt) {
c.$setValidity('server', true);
});
Yes, I have run into this since this post! I handled it with this after the modelstate watch, likely the same place you have the event bindings:
scope.$watch(attrs.ngModel, function(oldValue, newValue) {
if (oldValue != newValue) { c.$setValidity('server', true);
c.$error.server = [];
}
});
So, when model changes (whether by user in the control or by something else modifying the model), reset the validity for the control.
Hello Daniel,
I need to do server side validation and my validation condition is I need to check the unique value with the combination of two fields in the table. Like it must not have same name for same category.
Can you give me idea for my condition?
Thanks,
Arpana
Arpana - this can be done with custom validation attributes. Inherit ValidationAttribute and override IsValid. Inside IsValid, do the check. (See this article: http://www.codeproject.com/Articles/301022/Creating-Custom-Validation-Attribute-in-MVC)
The other approach would be to not validate, but simply add a constraint to the database table. Saves that violate the constraint will fail. The downside of this is that it may be difficult to separate this error from, say, a connection timeout and show the user in a meaningful way. The upside is that you do not incur two database calls for the save (one to check for unique and another to save).
Hope that helps!
Do you please have a working example of this somewhere? I think the idea is great, but it's really difficult to re-create the functionality by following the article.
I've ran into several problems and did some fixes. E.G.:
1. Added "$scope.item = {};" in controller
2. Added "$scope.modelState = {};" in controller
3. Changed "$scope.modelState = data.ModelState;" with "$scope.modelState = data.modelState;"
I'm now stuck with nothing happening after a 400 Bad Request is returned from server. I suspect it has to do with "form" being undefined, but I don't know how to fix that...
@Ion I've actually had to revisit this recently myself and have a few tweaks, such as using an httpInterceptor to automatically handle the wireup in angular. Also, this was written before controller-as syntax, which changes things a bit. I'll try to work up a post to revisit this soon, and maybe do something more readily usable in github/nuget In the meantime, a few tips:
*Your form tag and inputs need to have a name. This name becomes the way you reference it throughout the app.
*Conceptually the _form_ and the _model_ are different, completely separate things. Angular validation happens on the form, not the model.
*Re #3, the casing on data.ModelState will be important. If your service is returning it with a capital M, then it will be data.ModelState. You can look at it with a console.log(data) and that will help.
Post a Comment