Home | Send Feedback

Form validation with Angular and Spring Boot

Published: January 26, 2017  •  Updated: December 04, 2018  •  angular, ionic4, spring, java, javascript

Validating user input is an important part in an application. The validation process makes sure that the data the user enters is sensible and valid. In this article we explore the possibilities to do client side validation in an Angular (Ionic) app and server side validation with Spring and the Java Bean Validation (JSR 303) framework.

Client

For the client we create an Ionic app with the blank starter template

ionic start validation blank

We use a reactive/model driven form for this example. First we create the template for the form. Open the file src/app/home/home.page.html and create this simple form.

<ion-content padding>
 <form [formGroup]="registrationForm" (submit)="register()" novalidate>

   <ion-list>
      <ion-item>
        <ion-label position="stacked">Username</ion-label>
        <ion-input formControlName="username" type="text"></ion-input>
      </ion-item>

      <ion-item>
        <ion-label position="stacked">Email</ion-label>
        <ion-input formControlName="email" type="email"></ion-input>
      </ion-item>

      <ion-item>
        <ion-label position="stacked">Age</ion-label>
        <ion-input formControlName="age" type="number"></ion-input>
      </ion-item>
   </ion-list>

    <ion-button expand="block" type="submit" [disabled]="!registrationForm.valid">
      Create an Account
    </ion-button>

 </form>
</ion-content>

https://github.com/ralscha/blog/blob/master/validation/client/src/app/home/home.page.html

The form consists of three input elements: username, email and age field. We bind the form with the formGroup directive to a FormGroup instance in the code. The three input elements are bound to FormControl instances with the formControlName directive. On the form the submit event is bound to a register() function. A click/tap on the submit button at the bottom triggers this event. The disabled attribute of the button is bound to the valid property of the FormGroup class. This flag has the value true when all the containing FormControls of this group don't have any validation errors. In that case disabled is set to false and the user can click/tap on the button.

In the src/app/home/home.page.ts file we add the TypeScript code for the form. We do this in the constructor and, because we want to utilize the FormBuilder, we inject it into our class.

export class HomePage {
 public registrationForm: FormGroup;
 private readonly emailRegex = " ..... ";

 constructor(formBuilder: FormBuilder) {
   this.registrationForm = formBuilder.group({
     username: ['', [Validators.required, Validators.minLength(2), Validators.maxLength(30)]],
     email: ['', [Validators.required, Validators.pattern(this.emailRegex)]],
     age: ['', [Validators.required]]
   });
}

https://github.com/ralscha/blog/blob/master/validation/client/src/app/home/home.page.ts

The FormBuilder saves us from writing a few characters but it's not a requirement for the model driven approach. Without the builder the code would look like this.

this.registrationForm = new FormGroup({
 username: new FormControl('', [Validators.required, Validators.minLength(2), Validators.maxLength(30)]),
 email: new FormControl('', [Validators.required, Validators.pattern(this.emailRegex)]),
 age: new FormControl('', [Validators.required])
});

https://github.com/ralscha/blog/blob/master/validation/client/src/app/home/home.page.ts

We have to make sure that the names we use for the FormGroup and FormControl matches the names in the template. The first argument of the FormControl is the initial value that is shown to the user when the form is presented the first time. We set it to the empty string '' for all fields. The next argument is an array of validators. Angular provides a few built in validators and most of them are static methods of the Validators class. Here is a project that provides many more useful validators for Angular applications: https://github.com/yuyang041060120/ng2-validation

We specify that the username is required and must have a length between 2 and 30 characters. The email is required and must match a regular expression pattern. The age field is required.

Next we implement the register() method

register() {
 console.log(this.registrationForm.value);
}

At the moment we only print out the form values to the console. The FormControl class contains a value property that returns the form data as an object

{
  age:"18"
  email:"test@test.com"
  username:"username"
}

Next we start the app with the command ionic serve, enter some data, click on the submit button and when everything is valid you should see the output in the developer console. The button is disabled when there are validation errors.

The form in its current state is not very user-friendly. The user only notices that the button is disabled but he has no idea why. In this step we add error messages to give the user feedback what's wrong with the input.

In the file src/app/home/home.page.html below the ion-input field we add several ion-item components for every possible validation error.

      <ion-item>
        <ion-label position="stacked">Username</ion-label>
        <ion-input formControlName="username" type="text" [class.invalid]="isInvalidAndDirty('username')"></ion-input>
      </ion-item>
      <ion-item class="error-message" *ngIf="hasError('username', 'required')">
        <ion-label>
          Username is required
        </ion-label>
      </ion-item>
      <ion-item class="error-message" *ngIf="hasError('username', 'minlength')">
        <ion-label>
          Username must be at least 2 characters long
        </ion-label>
      </ion-item>
      <ion-item class="error-message" *ngIf="hasError('username', 'maxlength')">
        <ion-label>
          Username cannot be longer than 30 characters
        </ion-label>
      </ion-item>
      <ion-item class="error-message" *ngIf="hasError('username', 'usernameTaken')">
        <ion-label>
          Username is already taken
        </ion-label>
      </ion-item>

https://github.com/ralscha/blog/blob/master/validation/client/src/app/home/home.page.html#L14-L37

The template calls two functions in the TypeScript code to check if a field has errors. On the input field itself the CSS class invalid is added when the function isInvalidAndDirty returns true.

The method is implemented in the TypeScript code

  isInvalidAndDirty(field: string) {
    const ctrl = this.registrationForm.get(field);
    return !ctrl.valid && ctrl.dirty;
  }

home.page.ts

The get function of the FormGroup returns the FormControl with the given name. The function then checks if the FormControl is not valid and the user changed the value (dirty).

Below the input field, messages are displayed when the function hasError returns true.

  hasError(field: string, error: string) {
    const ctrl = this.registrationForm.get(field);
    return ctrl.dirty && ctrl.hasError(error);
  }

home.page.ts

This function works very similar to the isInvalidAndDirty function. It fetches the FormControl and checks if the user entered some data (dirty) and if it contains a validation error with the given name.

When you enter some invalid data, you should now see an error message.

Validation messages


Custom validator

Next we want to validate the age field so that the user only enters numbers equal or bigger than 18. Because Angular does not have a built in validator for that, we have to write our own validator. Create a new TypeScript file and open it in an editor.

import {AbstractControl} from '@angular/forms';

export class AgeValidator {

  static validate(minAge: number) {
    return (control: AbstractControl): { [key: string]: boolean } => {
      if (!control.value || 0 === control.value.length) {
        return null;
      }

      if (control.value >= minAge) {
        return null;
      }
      return {'notOldEnough': true};
    };
  }

}

age-validator.ts

A validation function receives the control as an argument and returns an object when there is a validation error or null when the value is valid.
With control.value the code has access to the value the user entered. The function checks first if the value is not empty, because we don't want the age validator to show an error message in this case. The required validator already takes care of this case. Then the function checks if the value is equal or bigger than the minimum. If not it returns the object {"notOldEnough": true}. Because we want a reusable validator, we don't hard code the minimum age value in this function, instead we create a factory function that encloses the minAge argument and returns the validation function.

Back in the home.page.ts file we add the validator to the age control and specify the minimum age.

 public readonly minAge: number = 18;
 
 ...

 age: ['', [Validators.required, AgeValidator.validate(this.minAge)]]

and in template file we add a new error message

      <ion-item class="error-message" *ngIf="hasError('age', 'notOldEnough')">
        <ion-label>
          You must be at least {{minAge}} years old!
        </ion-label>
      </ion-item>

Notice that the string 'notOldEnough' corresponds with the key in the object the validator returns in case of an error. You should now see an error message when you enter an age less than 18.

Age validator


Custom asynchronous validator

Angular does not only support synchronous validators like the age validator, but also asynchronous validators. An asynchronous validator is a function that returns a Promise or an Observable and is especially useful when the client has to ask a server to validate a value.
For this example we want to check if the username is unique. Most of the time a user database is stored on a server and the client cannot determine if the username is already taken by someone else.

We start with the server and create a simple Spring Boot application. As usual, I create my Spring Boot applications with the website https://start.spring.io/. For this example we only need the Web dependency.

We don't access a real database, the code just checks a Set if the username is already taken or not. We create a RestController and a method mapped to the /checkUsername URL. This method checks if the value is an element in the set and returns true or false.

@RestController
@CrossOrigin
public class RegistrationController {

  private final Set<String> existingUsernames = new HashSet<>();

  RegistrationController() {
    this.existingUsernames.add("admin");
    this.existingUsernames.add("user");
  }

  @GetMapping("/checkUsername")
  public boolean checkUsername(@RequestParam("value") String value) {
    return this.existingUsernames.contains(value);
  }

RegistrationController.java

Start the server with ./mvnw spring-boot:run or start the main class in an IDE. Back on the client we create a new TypeScript file and add the following code for the asynchronous validator.

import {HttpClient} from '@angular/common/http';
import {AbstractControl} from '@angular/forms';
import {environment} from '../environments/environment';
import {Injectable} from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class UsernameValidator {

  private timeout;

  constructor(private readonly http: HttpClient) {
  }

  validate(control: AbstractControl): Promise<{ [key: string]: boolean }> {
    clearTimeout(this.timeout);

    const value = control.value;

    // do not call server when input is empty or shorter than 2 characters
    if (!value || value.length < 2) {
      return Promise.resolve(null);
    }

    return new Promise((resolve, reject) => {
      this.timeout = setTimeout(() => {
        this.http.get<boolean>(`${environment.serverURL}/checkUsername?value=${control.value}`)
          .subscribe(flag => {
              if (flag) {
                resolve({'usernameTaken': true});
              } else {
                resolve(null);
              }
            },
            (err) => {
              console.log(err);
            }
          );
      }, 200);
    });
  }

}

username-validator.ts

The validator sends a GET request to the server endpoint with the username as a query parameter and returns a Promise that resolves to either null when the server returns false or to the object {'usernameTaken': true} when the server returns true.

The code first checks if the user input is not empty and has a length of at least 2 characters. There are already validators for these two cases in place so we ignore them here. Validators are called everytime the user enters some characters, this generates a lot of HTTP requests. To reduce the number of requests the http.get call is wrapped in a setTimeout function that waits 200 milliseconds before it runs the code. When Angular calls the validator again during these 200 milliseconds the previous call is cancelled with clearTimeout(this.timeout).

Next we inject this new validator into the home component class and add it to the username array as the third(!) element. It is not going to work when you add asynchronous validators to the array of the synchronous validators (2nd element).

  constructor(formBuilder: FormBuilder, usernameValidator: UsernameValidator,
              private readonly http: HttpClient, private readonly toastCtrl: ToastController) {

    this.registrationForm = formBuilder.group({
      username: ['', [Validators.required, Validators.minLength(2), Validators.maxLength(30)]
        , usernameValidator.validate.bind(usernameValidator)],

home.page.ts

The bind might look a bit strange, but it is necessary because we have to reference functions and if we add the validator like this usernameValidator.validate it loses the context. When Angular calls the function this would be bound to something else and it throws an error, because we have instance variables in our validator (http, timeout). To make sure that this is bound to the usernameValidator instance the code calls the built in bind function which does return a new function that wraps the validate function and binds this to the provided argument.

Next we add a new validation message to the template

      <ion-item class="error-message" *ngIf="hasError('username', 'usernameTaken')">
        <ion-label>
          Username is already taken
        </ion-label>
      </ion-item>

home.page.html

The parameter 'usernameTaken' matches the key in the validation object returned from the validator. When everything is in place and you enter either admin or user in the username field a validation message appears.

Username validator


Server validation

Because a server should not trust the client and it's quite easy to change HTTP requests, we want the server to validate the user input again when it receives the data. In a Java application we can utilize the Java Bean Validation framework for that. The necessary library is already loaded and automatically configured by Spring Boot. All what's left for the developer to do is adding the necessary annotations to the fields of the bean.

public class Registration {

  @NotBlank
  @Size(min = 2, max = 30)
  private String username;

  @NotBlank
  @Email
  private String email;

  @Min(18)
  @NotNull
  private Integer age;

Registration.java

This code mirrors the validation on the client side. Every field is required, username must have a length between 2 to 30 characters, the email string has to contain a valid email address and the age field must have a value of at least 18.

In the RegistrationController class we add a post mapping for the URL /register. Because the client sends the data as JSON we annotate the parameter with @RequestBody. The @Valid annotation triggers a validation run as soon as the message is deserialized. The second parameter, BindingResult, contains the validation messages. Because there is no built in validation for checking if the username is already taken the application does it here. Although the Java Bean Validation framework supports custom validators and if an application uses the same validation in multiple places writing such a custom validator would be a better approach.

  @PostMapping("/register")
  public Map<String, Set<String>> register(@Valid @RequestBody Registration registration,
      BindingResult result) {

    Map<String, Set<String>> errors = new HashMap<>();

    if (this.existingUsernames.contains(registration.getUsername())) {
      errors.computeIfAbsent("username", key -> new HashSet<>()).add("usernameTaken");
    }

    for (FieldError fieldError : result.getFieldErrors()) {
      String code = fieldError.getCode();
      String field = fieldError.getField();
      if (code.equals("NotBlank") || code.equals("NotNull")) {
        errors.computeIfAbsent(field, key -> new HashSet<>()).add("required");
      }
      else if (code.equals("Email") && field.equals("email")) {
        errors.computeIfAbsent(field, key -> new HashSet<>()).add("pattern");
      }
      else if (code.equals("Min") && field.equals("age")) {
        errors.computeIfAbsent(field, key -> new HashSet<>()).add("notOldEnough");
      }
      else if (code.equals("Size") && field.equals("username")) {
        if (registration.getUsername().length() < 2) {
          errors.computeIfAbsent(field, key -> new HashSet<>()).add("minlength");
        }
        else {
          errors.computeIfAbsent(field, key -> new HashSet<>()).add("maxlength");
        }
      }
    }

    if (errors.isEmpty()) {
      System.out.println(registration);
    }

    return errors;
  }

RegistrationController.java

What the code does is mapping the validation errors from the framework to the names we use on the client side. For example when the age field violates the Min validation it adds the string "notOldEnough" to the result, the same string we use in the client side validator.

The method returns an object that contains the field name as the key and an array of validation errors. Spring, with the help of Jackson, automatically converts this to the following JSON.

{email: ["pattern"], age: ["notOldEnough"], username: ["usernameTaken"]}

On the client we need to add code to the register() function

  register() {
    console.log(this.registrationForm.value);
    this.http.post<object>(`${environment.serverURL}/register`, this.registrationForm.value)
      .subscribe(async (data) => {
        for (const fieldName of Object.keys(data)) {
          const serverErrors = data[fieldName];

          const errors = {};
          for (const serverError of serverErrors) {
            errors[serverError] = true;
          }

          const control = this.registrationForm.get(fieldName);
          control.setErrors(errors);
          control.markAsDirty();
        }

        if (this.registrationForm.valid) {
          const toast = await this.toastCtrl.create({
            message: 'Registration successful',
            duration: 3000
          });
          toast.present();
        }
      });
  }

home.page.ts

First the function sends a POST request to the server with the values of the form. Then it handles the response, if there are any validation errors it fetches the FormControl with this.registrationForm.get(fieldName) and sets an error object with control.setErrors. Because the template also checks the dirty flag before it shows the validation messages, the code calls control.markAsDirty(). At the end of the method a toast message is displayed when there are no validation errors.

To test the validation on the server we need to disable the client validations. We can do that by removing all the validators from the formBuilder statement.

this.registrationForm = formBuilder.group({
  username: [],
  email: [],
  age: []
});

Now enter some invalid data and click on submit. After a short delay, you see the validation messages.

You find the complete source code for this example on GitHub.