AngularJS has a wonderful front end validation in built.
But as we all know (do we all?) that client side validation is not enough and must be backed up by server side validation. So how do we do it?
AngularJS validation is almost a dream.
But like a nightmare, I couldn't figure out how to user server side validation instead of client side. Until now.
So let's get started.
The technologies being used.
First the BackEnd
Now the FrontEnd
But as we all know (do we all?) that client side validation is not enough and must be backed up by server side validation. So how do we do it?
AngularJS validation is almost a dream.
But like a nightmare, I couldn't figure out how to user server side validation instead of client side. Until now.
So let's get started.
The technologies being used.
- FrontEnd - AngularJS
- BackEnd - Spring Framework
I am not using any DB, just a Map to save data in memory.
First the BackEnd
- Create your bean Person with id, name, and age.
Use javax.validation.constraints.* to annotate name, and age.
package domain; import java.io.Serializable; import javax.validation.constraints.Max; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; public class Person implements Serializable { public Person() { super(); } public Person(Long id, String name, Integer age) { super(); this.id = id; this.name = name; this.age = age; } /** * */ private static final long serialVersionUID = -1293372630653301413L; private Long id; @NotNull @Size(min=5, max=20) private String name; @NotNull @Min(18) @Max(100) private Integer age; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } }
As you can see, name has to be minimum 5 characters, max 20, and is required. age has to be between 18 and 100, and required. - Remove client side validation if any. Reason: I don't want to duplicate my validation logic (error possibilities).
- Create Our custom validator PersonValidator.
package domain.validator; import java.util.HashMap; import java.util.Map; import java.util.Set; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; import javax.validation.Validation; import javax.validation.ValidatorFactory; import org.springframework.validation.Errors; import org.springframework.validation.Validator; import domain.Person; import exception.MyValidationException; /** * Validator for Person. * @author Tathagat * */ public class PersonValidator implements Validator { @Override public boolean supports(Class clazz) { return Person.class.isAssignableFrom(clazz); } @Override public void validate(Object target, Errors errors) { // first do the normal annotation based validation ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); javax.validation.Validator validator = factory.getValidator(); Set
The first 3 lines of the method validate() calls the standard validation based on javax.validation.constraints.*. If we don't do this, our standard validation is not called which is a bummer. Then the validationErrors need to be thrown. In the ExceptionHandlerController class below, these expceptions will be caught.> validationErrors = validator.validate(target); // throw all violations so they can be caught and processed if (!validationErrors.isEmpty()) { throw new ConstraintViolationException(validationErrors); } // here comes my custom validation Map customValidationErrors = new HashMap (); Person person = (Person) target; if ("Hitler".equalsIgnoreCase(person.getName())) { customValidationErrors.put("name", "Are you kidding me?"); } // throw once all custom validations are done if (!customValidationErrors.isEmpty()) { throw new MyValidationException(customValidationErrors); } } }
Next (After all annotation based validations have passed (and therefore no exceptions have been thrown) we can do our custom validation. Note that both in one go are not possible.
I am making sure that Hitler cannot be stored as a name. If there is a violation, I put it in a map and wrap it in a custom Exception MyValidationException (see below) and throw it. - Create PersonController which supporst REST calls using Spring
package controller; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; import javax.validation.Valid; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import domain.Person; import domain.validator.PersonValidator; @RestController @RequestMapping("api") public class PersonController { /** * some default values */ public PersonController() { super(); atomicId = new AtomicLong(0); long id = atomicId.getAndIncrement(); persons.put(id, new Person(id, "Tathagat", 36)); id = atomicId.getAndIncrement(); persons.put(id, new Person(id, "Betty", 35)); personValidator = new PersonValidator(); } private PersonValidator personValidator; @InitBinder public void initBinder(WebDataBinder binder) { binder.setValidator(personValidator); } private AtomicLong atomicId = new AtomicLong(); private Map
The constructor adds 2 persons to the persons Map (which I am using instead of a DB). It also initializes our custom validator.persons = new HashMap<>(); // persons GET - get all @RequestMapping(value="/persons", method=RequestMethod.GET) public List getAll() { List allPersons = new LinkedList<>(); Set keys = persons.keySet(); for (Long key : keys) { allPersons.add(persons.get(key)); } return allPersons; } // persons POST - create new @RequestMapping(value="/persons", method=RequestMethod.POST) public void create(@Valid @RequestBody Person person) { // if ID is null, then it's an insert if (person.getId()==null) { long id = atomicId.getAndIncrement(); person.setId(id); } else { // for update, we remove it first persons.remove(person.getId()); } persons.put(person.getId(), person); } // persons/:id GET - get single @RequestMapping(value="/persons/{id}", method=RequestMethod.GET) public Person get(@PathVariable Long id) { return persons.get(id); } // persons/:id DELETE - delete existing @RequestMapping(value="/persons/{id}", method=RequestMethod.DELETE) public void delete(@PathVariable Long id) { persons.remove(id); } }
There are 4 REST methods which conform to the REST methods as defined by the AngularJS resource: getAll (/api/persons - GET), create (/api/persons - POST), get (/api/persons/:id - GET), delete (/api/persons/:id - DELETE).
The create method parameter has an extra annotation @Valid which basically says, validate the passed parameter.
The method initBinder sets the validator that should be called when the create method is called. - Create the ExceptionHandlerController class. Remember the exceptions being thrown by PersonValidator? They will be caught and processed here.
package controller; import java.util.HashMap; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; import org.springframework.http.HttpStatus; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import exception.MyValidationException; @ControllerAdvice public class ExceptionHandlerController { @ResponseStatus(HttpStatus.BAD_REQUEST) @ResponseBody @ExceptionHandler({MethodArgumentNotValidException.class, ConstraintViolationException.class, MyValidationException.class}) public Map
The @ControllerAdvice advices the controller. @ExceptionHandler defines which exceptions should be handled here.processCalidationExceptions(HttpServletRequest req, Exception ex) { Map validationErrors = new HashMap<>(); if (ex instanceof MethodArgumentNotValidException) { for (FieldError fieldError : ((MethodArgumentNotValidException)ex).getBindingResult().getFieldErrors()) { validationErrors.put(fieldError.getField(), fieldError.getDefaultMessage()); } } if (ex instanceof ConstraintViolationException) { for (ConstraintViolation violation : ((ConstraintViolationException) ex).getConstraintViolations()) { validationErrors.put(violation.getPropertyPath().toString(), violation.getMessage()); } } if (ex instanceof MyValidationException) { validationErrors.putAll(((MyValidationException)ex).getErrors()); } return validationErrors; } }
As you can see, 3 types of exceptions are being caught.
From all of them, I take out the field name and the error message and put it in a Map and return that.
@ResponseBody indicates a method return value should be bound to the web response body. Basically the returned Map will be available in the front end on validation error. - For the sake of completion of the backend, here is MyValidationException.
package exception; import java.util.Map; public class MyValidationException extends RuntimeException { /** * */ private static final long serialVersionUID = 8347119250363399412L; private Map
The important thing is that this is a RuntimeException so I can throw it without declaring it. It just holds my custom errors (in a Map).errors; public MyValidationException(Map errors) { this.errors = errors; } public Map getErrors() { return errors; } }
Now the FrontEnd
- First the module myApp.js
var app = angular.module("myApp", ['ngResource']);
- Now the factory for Person, factory.js
app.factory('Person', function($resource) { return $resource('/api/persons/:id'); });
- the controller.js. This is where the magic happens.
app.controller("personController", function($scope, Person) { $scope.persons = Person.query(); clearValidationErrorMessages = function() { $scope.personForm.$setPristine(true); $scope.serverErrors=""; } $scope.load = function(id) { $scope.person = Person.get({id: id}); clearValidationErrorMessages(); $('#personFormDiv').show(); } $scope.loadForNew = function() { $scope.person = new Person(); clearValidationErrorMessages(); $('#personFormDiv').show(); } $scope.delete = function(id) { Person.delete({id: id}); $scope.persons = Person.query(); } $scope.insert_update = function() { Person.save($scope.person, function() { $scope.persons = Person.query(); clearValidationErrorMessages(); $('#personFormDiv').hide(); }, function(errors) { //console.log(errors); $scope.serverErrors=errors.data; for (var errorKey in errors.data) { //console.log(errorKey + ':' + errors.data[errorKey]); $scope.personForm[errorKey].$dirty=true; } }); } });
As soon as the page loads, all the persons are loaded using $scope.persons = Person.query(); persons are then used in the index.html below to create a table.
When the EDIT button in the GUI is clicked, the load method is called which uses the REST to load that particular Person ($scope.person = Person.get({id: id});). It also clears all the validation error messages (if any), and shows the edit div (Which was hidden until now).
When the NEW button is clicked, loadForNew is called. This will instantiate an empty Person and clear the error messages. It will also show the hidden div.
DELETE will call delete which will call the REST endpoint for delete.
On SAVE, insert_update will be called. This will save the loaded person using the REST endpoint. On success, the persons will be loaded again, validation error messages will be cleared, and the div will be hidden again. If there is an error (remember the map we were returning from the ExceptionHandlerController class?) we will loop through the errors and assign the individual fields as dirty. Also the error messages map is stored in the scope to fetch the error message. - finally the GUI, index.html
<!DOCTYPE html> <html> <head> <script src="https://code.jquery.com/jquery-2.1.3.min.js"></script> <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.26/angular.min.js"></script> <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.26/angular-resource.js"></script> <script src="scripts/myApp.js"></script> <script src="scripts/person/factory.js"></script> <script src="scripts/person/controller.js"></script> </head> <body> <div ng-app="myApp" ng-controller="personController"> <p>{{ persons }}</p> <button type="submit" ng-click="loadForNew()">NEW</button> <table border="0"> <thead> <tr> <td>ID</td> <td>Name</td> <td>Age</td> <td></td> <td></td> </tr> </thead> <tbody> <tr ng-repeat="person in persons"> <td> {{person.id}} </td> <td> {{person.name}} </td> <td> {{person.age}} </td> <td><button type="submit" ng-click="load(person.id)">EDIT</button></td> <td><button type="submit" ng-click="delete(person.id)">DELETE</button></td> </tr> </tbody> </table> <br/><br/><br/><br/> <div id="personFormDiv" style="display:none;"> <form name="personForm" novalidate ng-submit="insert_update()"> <table border="0"> <tr> <td>ID</td> <td> <input type="text" READONLY name="id" ng-model="person.id" /> </td> </tr> <tr> <td>Name</td> <td> <input type="text" name="name" ng-model="person.name" /> <span ng-show="personForm.name.$dirty">{{serverErrors['name']}}</span> </td> </tr> <tr> <td>Age</td> <td> <input type="number" name="age" ng-model="person.age" /> <span ng-show="personForm.age.$dirty">{{serverErrors['age']}}</span> <span ng-show="personForm.age.$error.number">Please use a number.</span> </td> </tr> </table> <button type="submit" ng-disabled="personForm.$invalid">SAVE</button> </form> </div> </div> </body> </html>
The error messages are only shown when the field is dirty. The message is fetched from the map which we stored in the scope (Which in turn comes from ExceptionHandlerController).<span ng-show="personForm.name.$dirty">{{serverErrors['name']}}</span>