Friday, 3 March 2017

Using Spring Validation in Angular

I was recently working on a web project where I needed support for substantial modal web dialogs inside web pages. My first approach was to use what what was familiar to me which was  JSP with spring MVC controllers. The problem is this isn't very convenient with modal components as you  you have to manually bundle up  the value in the form with java script code and post it yourself.

So I started exploring Angular JS as a way of doing this and basically it took over my life (in a good way I think).

One are I struggled with was validation but I found a solution I was happy with and hence this post.

Problem

Since I started with Spring I had my models annotated with validation constraints like the example below :

 class User  
 {  
   // ...  
   @Size(min=1, message="First Name must be provided.")  
   private String firstName;  

   @Size(min=1, message="Last Name must be provided.")  
   private String lastName;  

   private String organisation;  
   @Size(min=1, message="Email must be provided.")  

   private String email;  
   @Size(min=1, message="Password must be provided.")  

   private String password;  
   private boolean isAdmin;  
 }  

Then the controller can validate objects of this type using the SpringMVC magic as follows:

 
   @RequestMapping(value="/user",method=POST)  
   public @ResponseBody ModelAndView editUser(  
       @Valid @RequestBody User user,  
       BindingResult    result)  
   {  
     if ( result.hasErrors() )  
     {  
       return ...  
     }  
    ...  

The BindingResult is then available in the JSP so you can write code in your form like this to display the errors (using some tag library stuff).

  
    <form:input path="firstName"   
       cssClass="field input medium"  
       cssErrorClass="field input medium error" />  
    <form:errors path="firstName" cssClass="error" element="p" />  

But if we aren't using JSP then how do we get these errors out? If we are using Angular then we post the form data as JSON asynchronously and the page isn't reloaded.

Outline

When I searched for a solution to this problem, the first things that came up were techniques for implementing validation in Angular. While its good to validate the content before leaving the page the problem is you still have to validate the content at the server as otherwise a rogue user could mess you up.

So there is some desire to not implement this validation in two places and to report the validation errors from the server in the client.

Server

So on the server we define a new type that will carry the validation results.

  
 public class ValidationResponse  
 {  
   public String getStatus()  
   {  
     return status;  
   }  
   public void setStatus(String status)  
   {  
     this.status = status;  
   }  
   public List<ObjectError> getErrorMessageList()  
   {  
     return this.errorMessageList;  
   }  
   public void setErrorMessageList(List<ObjectError> errorMessageList)  
   {  
     this.errorMessageList = errorMessageList;  
   }  
   /**  
    * A general validation error not specific to a field  
    * @return The error text  
    */  
   public String getGeneralErrorText()  
   {  
     return generalErrorText;  
   }  
   /**  
    * A general validation error not specific to a field  
    * @param generalErrorText The error text  
    */  
   public void setGeneralErrorText(String generalErrorText)  
   {  
     this.generalErrorText = generalErrorText;  
   }  
   private String status;  
   private String generalErrorText;  
   private List<ObjectError> errorMessageList;  
 }  


Then the methods that accept REST POST calls and that will validate objects do something like this:

    
   @RequestMapping(value="/user.json",method=POST)  
   public @ResponseBody ValidationResponse editUser(  
       @Valid @RequestBody User user,  
       BindingResult    result)  
   {  
     ValidationResponse response = new ValidationResponse();  
     if ( result.hasErrors() )  
     {  
       response.setErrorMessageList(result.getAllErrors());  
       response.setStatus("FAIL");  
     }  
     else  
     {  
       ....  
     }  
     return response;  

In addition if you want to have an error that applies to the whole form rather than a specific field you can use the general text field in the ValidationResult above.

Web

In the form we define error spans for each field as follows. The error fields us ng-show to toggle if they will be displayed based on a hasError() method in the controller and display the content returned by a getError() method.

 
    <div ng-controller='registerController'>  
    <div class="form-group">  
      <label for="userFirstName">First name<span class="required">*</span></label>  
      <input class="form-control" ng-model="object.firstName" name="firstName" />  
      <span class="help-inline" ng-show="hasError('firstName')">{{getError("firstName")}}</span>  
    </div>  
    ...  
 </div>  

Angular Controller

As this pattern would be applied to every form, I created a base controller that the form controllers could extend to provide the validation methods.

The controller takes the resource and context (to build the URL) from its parameters when it is instantiated. The controller provides the hasError() and getError() methods as well as method for posting the updated object and checking if the result was an error.

 
 angular.module("MyApp").controller("formController",function($scope, $http, $q, object,context,resource)  
 {  
   $scope.formErrors = {};  
   $scope.context = context;  
   $scope.resource = resource;  
   $scope.object = object;  
   $scope.hasError = function(fieldName)  
   {  
     if (typeof ($scope.formErrors) != 'undefined')  
     {  
       return fieldName in $scope.formErrors;  
     }   
     else  
     {  
       return false;  
     }  
   }  
   $scope.getError = function(fieldName)  
   {  
     if (typeof ($scope.formErrors) != "undefined" && fieldName in $scope.formErrors)  
     {  
       return $scope.formErrors[fieldName];  
     }   
     else  
     {  
       return "";  
     }  
   }  
   $scope.postUpdate = function()  
   {  
    var deferred = $q.defer();  
     $scope.formErrors = [];  
     $http.post($scope.context + $scope.resource,$scope.object).then(  
       function(response)  
       {  
         if (response.data.status == "SUCCESS")  
         {  
           return deferred.resolve();  
         }   
         else  
         {  
           for (i = 0; i < response.data.errorMessageList.length; i++)  
           {  
             $scope.formErrors[response.data.errorMessageList[i].field] = response.data.errorMessageList[i].defaultMessage;  
           }  
         }  
       });  
     return deferred.promise;  
   };  
 });  

Then every controller that needs this will extend the form controller like this:

  
 angular.module("MyApp").controller("registerController",function($scope,$controller,$http,$rootScope,$location)  
 {  
   $scope.object = { }  
   angular.extend(this,$controller('formController', {$scope: $scope, object : $scope.object, context: '/MyApp', resource: '/register' }));  
   $scope.submit = function()   
   {  
     if ( $scope.object.password != $scope.passwordConfirm )   
     {   
        $scope.formErrors["passwordConfirm"] = "Passwords do not match";   
     }   
     else   
     {   
        $scope.postUpdate().then(  
          function($location.path('/registersuccessful') } );  
     }   
   }  
 });  

In this case the submit function also does some validation before sending the form content (using the postUpdate() method) to the server. When the postUpdate() completes it invokes the function for moving the URL to the success page (via the promise returned by the post method).

Conclusion

The benefits provided by the method are worth the small additional overhead. While I still find the syntax of Javascript a bit of a puzzle at times Angular is growing on me.

2 comments: