Client side validation is pretty handy. However, as we all know, you can’t fully trust any data sent to your server from a web client. As such, we generally duplicate validation in both the client and server side scenarios. This isn’t a big problem, but it does create a disconnect when the client validation passes, but then the server validation fails. Even in duplication, I still want server-side validation to play an integral part of the overall user experience.
There are many validation frameworks for JavaScript that are easy enough to hook into. Think back, though, on a previous blog post about validation of date times in Angular. Validation in Angular is, generally, nothing more than a directive. How, though, do we connect the dots? What does the directive look like and how do our server and Angular controller interact to make this a seamless experience?
For this scenario, our validation becomes two fold. We will need to process a response from the server that tells us there are errors and we will need to apply those errors to the appropriate form elements. Our directive will be simple in that it will apply event handlers to clear errors automatically.
Regarding a server response, we will expect the server to pass back a specific response indicating property-level errors which we can then map to our form elements. This response will also have a specific status code and status text so that we know a validation exception has occurred. In Angular, using the formController, we know our form element validators keyed off of the name attribute and can set validity of each element based on this.
As such, our service looks something like the service below. We can return an error object from an Http response to determine if an error and then apply the errors based on a list of errors. The code looks like this:
var validationService = function ($timeout) { var processServerErrors = function (formCtrl, errors) { if (errors && errors.length > 0) { for (var i = 0; i < errors.length; i++) { var key = errors[i].Key; var name = key.split('.').slice(-1); for (var j = 0; j < errors[i].Value.length; j++) { var formElement = formCtrl[name]; if (formElement) { var errorMessage = errors[i].Value[j]; formElement.$setValidity('server', false); formElement.$error.serverMessage = errorMessage; } } } } }; var isValidationError = function (error) { return error.status === 400 && (error.statusText == "validationException" || (error.data && error.data.length > 0 && error.data[0].Key)); }; return { processServerErrors: processServerErrors, isValidationError: isValidationError }; };
In order to utilize the validation service, our Angular controller would simply have some post processing after an Ajax request in the error handler:
function (error, status) { if (validationService.isValidationError(error)) { validationService.processServerErrors(vm.myForm, error.data); alert("Please correct the form errors shown."); } else if (error.data && error.data.exceptionMessage) { alert(error.data.exceptionMessage); } else { alert("Unable to Save"); } });
Basically, if we determine that the server did return validation errors, the service will using Angular’s form controller and the form element’s $setValidty/$error to set the “server” error and its “serverMessage.” If there was some other exception, we handle that as well.
If you’re using .NET, it’s pretty simple to return this response via a WebApi controller. I generally use a simple extension method to help serialize the ModelStateDictionary to a dictionary with camel cased keys. Effectively, I hook into this also by adding errors where my server-side, domain validation failed.
You can see this simple method here:
public static IEnumerable Errors(this System.Web.Http.ModelBinding.ModelStateDictionary modelState, bool fixName = false) { if (!modelState.IsValid) { return modelState.ToDictionary(kvp => fixName ? kvp.Key.Replace("model.", string.Empty).ToCamelCase() : kvp.Key.ToCamelCase(), kvp => kvp.Value .Errors .Select(e => string.IsNullOrWhiteSpace(e.ErrorMessage) ? "Invalid value entered." : e.ErrorMessage).ToArray()) .Where(m => m.Value.Count() > 0); } return null; }
With the errors in a simple dictionary, I then like to throw an exception w/ a specific response (note the reason phrase) serializing the dictionary:
var modelStateErrors = JsonConvert.SerializeObject(ModelState.Errors(true)); var response = new HttpResponseMessage(HttpStatusCode.BadRequest) { Content = new StringContent(modelStateErrors), ReasonPhrase = "validationException" }; throw new HttpResponseException(response);
Looking at that code and thinking of the response, I hope it’s relatively clear how the JavaScript then processes that response applying the validity state with respect to the dictionary keys and messages.
Now, to tie this all together, we still need a way for the user to indicate that the error no longer applies. To simplify this process, this is where a directive comes into play that takes advantage of DOM events to simply clear the server-error wherever there is any user interaction such as clicking or typing. As a result, this directive is pretty simple:
var keyTimer; var serverError = function () { var directive = { restrict: 'A', require: 'ngModel', link: function (scope, elm, attrs, ctrl) { var isClearOnClick = attrs.serverError = '' ? false : scope.$parent.$eval(attrs.serverError), clearError = function (apply) { if (ctrl.$invalid && ctrl.$error.server) { ctrl.$setValidity('server', true); ctrl.$setValidity('serverMessage', true); if (apply) { scope.$apply(); } } }; if (isClearOnClick) { elm.on('click', function () { clearError(); }); var inputGroup = elm.parents('.input-group'); var inputGroupBtn = $(inputGroup, '.input-group-btn > button:first'); inputGroupBtn.on('click', function () { clearError(true); }); }; // Always clear on key press. elm.on('keyup', function () { if (ctrl.$invalid && ctrl.$error.server) { clearInterval(keyTimer); keyTimer = setTimeout(function () { clearError(true); }, 200); } }); } }; return serverError; }
This directive, by default, will use $setValidity to clear errors on keyup. Optionally, it can also clear errors if the form element (or an associated input-group-btn) is clicked. Calling $setValidity on both ‘server’ and ‘serverMessage’ effectively removes those properties from the element’s $error collection.
Finally, we put this directive into our HTML like so:
<div class="form-group" ng-class="{ 'has-error': vm.myForm.date }"> <div class="input-group"> <input name="date" type="text" class="form-control date-field" uib-datepicker-popup="{{vm.dateFormat}}" ng-model="vm.date.date" is-open="vm.dateOpened" ng-click="vm.open()" ng-required="true" server-error="true" /> <span class="input-group-btn"> <button type="button" class="btn btn-default" ng-click="vm.open()"> <i class="glyphicon glyphicon-calendar" /> </button> </span> </div> <small class="error ng-hide" ng-show="vm.myForm.date.$error.required" ng-bind="'Date is required.'" /> <small class="error ng-hide" ng-show="vm.myForm.date.$error.server"> {{vm.myForm.date.$error.serverMessage}} </small> </div>
In the HTML, the server error is shown just like any other Angular validation message. Over the weekend, I’ll create a fully working code sample illustrating all of this tying together in part 2.