When basic form field validation isn't good enough, you might need to check some input against the database. And to handle that task, you'll need to implement an asynchronous validation solution.

Fortunately, it ain't difficult to do that in Angular. Of course, you'll need a back-end service to handle the validation as well.

I'll cover both sides of that equation in this guide.

But if you'd like to just get the code, you can do so on GitHub. The server-side source is here and the Angular code is here.

Remember: if you're having problems running the Angular app locally, be sure to check the README.

Please note: this guide is a part of an ongoing series of tutorials on how to create a customer relationship management (CRM) application from scratch. The GitHub code for each guide belongs to a specific branch. The master branch holds the entire application up to the current date. If you want to follow the whole series, just view the careydevelopmentcrm tag. Note that the website displays the guides in reverse chronological order so if you want to start from the beginning, go to the last page.

The Business Requirements

Your boss Smithers walks into your office, sits down, and sighs.

"You allowed users to enter contact email addresses without first checking to see if the email already exists?!?" he says, incredulously. "Why did you do that?"

You begin a reply: "Well that wasn't part of the original req-"

Smithers interrupts you.

"I don't want to hear it," he says. "Update that form right away so when the user enters a new contact, the application checks to see if the contact's email address is already in the system."

He begins to walk away but turns around just before he leaves.

"And don't let them add duplicate email addresses in the database!" he shouts.

Contactus Serviciticus

Let's start by looking at the Spring Boot contact service. I haven't covered that service yet in these guides, but it's out there.

You'll be shocked to learn that the contact service persists contact info to the database. It also retrieves contact data for users who need to see it.

And yeah, the contacts database uses MongoDB just like the the users database. It's all document-centric around here. None of that relational stuff.

For the purposes of this guide, I'll cover how to code the contact service so it checks if an email already exists in the database.

So here's how the whole thing will work: when a user fills out a contact's email address in the contact form, the Angular app will use the contact service to check if another contact already has that email address. If so, the field (and the form) is marked invalid.

Now, take a look at the contact model in the Spring Boot contact service:

@Document(collection = "#{@environment.getProperty('mongo.contact.collection')}")
public class Contact {

	@Id
	private String id;
	
	@NotBlank(message = "Please provide a first name")
	@Size(max = 50, message = "First name must be between 1 and 50 characters")
	private String firstName;
	
	@NotBlank(message = "Please provide a last name")
	@Size(max = 50, message = "Last name must be between 1 and 50 characters")
	private String lastName;
	
	@Email(message = "Please enter a valid email address")
	private String email;
	private List<Phone> phones = new ArrayList<Phone>();
	private List<Address> addresses = new ArrayList<Address>();
	
	private Source source;
	
	@Size(max = 50, message = "Source details cannot exceed 50 characters")
	private String sourceDetails;
	
	private ContactStatus status;
	private long statusChange;
	
	@NotEmpty(message = "Please include at least one line of business")
	private List<LineOfBusiness> linesOfBusiness;
	
	@Size(max = 50, message = "Company name cannot exceed 50 characters")
	private String company;
	
	@Size(max = 50, message = "Title cannot exceed 50 characters")
	private String title;
	private boolean authority;
	private SalesOwner salesOwner;

...

I left out the getters and setters in the interest of brevity.

But that's a fairly basic Java object representing a contact in the system. It includes important details about the contact, such as the person's name, company, title, status, and wheteher or not the person has purchasing authority.

Also, note that the object is married to a MongoDB document collection. You can tell that by looking at the @Document annotation at the top.

The SalesOwner object is a user in the system. But it's a flyweight object. In other words, it doesn't include all user details.

Of importance to you right now is that email field. That's going to get persisted to the MongoDB.

Later, you'll see how the service checks the database to see if an email address is already taken.

Graduating Validatorian

Next, it's time to update the ContactValidator class with a new method. 

    public boolean emailExists(String email) {
        boolean exists = false;
        
        if (email != null && !StringUtils.isBlank(email)) {
            Contact contact = contactRepository.findByEmail(email);
            exists = (contact != null);
        }
        
        return exists;
    }

The method's name speaks volumes. It determines if an email exists in the system and returns a true or false boolean accordingly.

It performs the check with a simple call to ContactRepository. The findByEmail() method doesn't even require an implementation. The framework is smart enough to know that the method name means "go find a contact by the given email address."

If a contact gets returned then that means the email exists. Otherwise, it doesn't exist.

Mission Controller

Next, add the following method to ContactsController.

    @PostMapping("/emailcheck")
    public ResponseEntity<?> emailCheck(@RequestBody Map<String, Object> inputData) {
        String email = (String)inputData.get("email");
        LOG.debug("Checking for existence of email " + email);
        
        Boolean bool = contactValidator.emailExists(email);
        
        return ResponseEntity.status(HttpStatus.OK).body(bool); 
    }

So that method maps to a /contact/email URI. Look at the @RequestMapping annotation at the top of the class (not shown here) to see where the /contact part comes from.

The POST method accepts any input as the body. That's why the body gets passed in as a Map instead of a specific type.

However, it's really looking for a JSON that specifies an email property. That's the email address that it will use to search for a contact in the database.

And, once again, if it finds a contact, that means that email address is already taken.

The method itself just returns a boolean to the calling client.

Angularity

So much for the coding on the service side. Now it's time to do the work on the client side.

Switch over to your Microsoft Visual Studio IDE and edit contact.service.ts. Add a method for checking email.

  doesEmailExist(email: string): Observable<boolean> {
    let url = `${baseUrl}/contact/emailcheck`;

    let content: any = {};
    content.email = email;

    let response$: Observable<boolean> = this.http.post<boolean>(url, content);

    return response$;
  }

That method accepts an email address string. Then, it goes out to the contact service to check if another contact already has that email address.

The method returns a boolean: true if the email exists, false if it doesn't. The boolean is wrapped in an Observable so a subscriber will need to extract it.

That content variable holds the payload. It just holds a single property (email) that the downstream service expects at the specified endpoint.

And since it's sending a payload, it uses a POST method.

And remember: you'll need to configure baseUrl in your environment files. Make it the base URL of where you've deployed the Spring Boot contact service.

Next, edit basic-info-form.component.ts. Add the async validator to the email field in the FormGroup object.

    this.basicInfoFormGroup = this.fb.group({
      'firstName': ['', [Validators.required, Validators.pattern('[A-Za-z \-\_]+')]],
      'lastName': ['', [Validators.required, Validators.pattern('[A-Za-z \-\_]+')]],
      'email': ['', [Validators.pattern("^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$")],
        [this.emailExistsValidator()],
       'blur'
      ],
      'source': ['', [Validators.required]],
      'sourceDetails': ['', [Validators.pattern('[A-Za-z0-9 \-\_]+')]],
      'status': ['NEW', [Validators.required]],
      'lineOfBusiness': [''],
      'authority': ['false'],
      'title': ['', [Validators.pattern('[A-Za-z\-\_]+')]],
      'company': ['', [Validators.pattern('[A-Za-z0-9 \-\_]+')]]
    });

Hone in on the email field there. That's where the magic is happening.

Unlike the other fields in the form, the email field uses all three properties in AbstractControlInterface

The first property is an array of synchronous validators. That array only contains one element: it's a regex that validates the format of the email address.

The next property is an array of asynchronous validators. It also only contains one element as well: the email check. You'll create the code for the method it's referencing in a moment.

The final property (updateOn) tells Angular when to update the control. In this case it updates on "blur" or when the user leaves the field after entering it.

Next, add the code for emailExistsValidator().

  private emailExistsValidator(): AsyncValidatorFn {
    return (control: AbstractControl): Observable<ValidationErrors | null> => {
      return of(control.value).pipe(
        delay(500),
        switchMap((email) => this.contactService.doesEmailExist(email).pipe(
          map(emailExists => emailExists ? { emailExists: true } : null)
        ))
      );
    };
  }

Well that one's fairly awkwardly worded, isn't it?

Here's the TL;DR of what's going on there: a method accepts a form control as input and returns an Observable that holds validation errors if they exist. If no errors exist, it returns null.

In this case, the input control is the email field. The method invokes the doesEmailExist() method on ContactService to find out if that email is already somewhere in the contacts database. 

Remember, though, that the method in ContactService returns an Observable. The map rxjs operator that you see above takes that Observable, gets the boolean from it, and returns another Observable based on whether that boolean is true of false.

The new Observable will be null if the boolean is false (meaning the email address doesn't exist). If that happens, the field is valid.

If the boolean is true, the method creates a new single-property object that associates an emailExists property with the value of true. 

That's a simple key/value pair that tells Angular two things:

  • Yes, there's an error on the field
  • Here's the type of error

The code uses a descriptive name for the key: emailExists. That makes it easy for other developers to figure out what's going on there.

The point of the delay rxjs operator you see above is to prevent every single keypress from causing a network request to the Spring Boot service. That 500 you see in parentheses means the app will wait 500 milliseconds (or half a second) before making the call to check if the email exists.

And switchMap() is used to cancel in-flight requests. Once the user types another character, the app no longer cares if the last input value is an email that exists.

Next, update the HTML to support the new validator.

  <div class="vertical-form-field">
    <div class="label">Email</div>
    <div>
      <mat-form-field appearance="fill" class="no-label-field" style="width:50%">
        <input formControlName="email" matInput placeholder="Enter email address" maxlength="50">
        <mat-error *ngIf="basicInfoFormGroup.get('email').hasError('emailExists'); else otherErrorCheck">That email already exists!</mat-error>
        <ng-template #otherErrorCheck>
          <mat-error *ngIf="basicInfoFormGroup.controls['email'].invalid">Please enter a valid email address</mat-error>
        </ng-template>
      </mat-form-field>
    </div>
  </div>

Look carefully at the code above and you'll see there are now two <mat-error> elements.

The first <mat-error> element reports the error if the email already exists.

The second <mat-error> element reports all other email validation errors with a generic "Please enter a valid email address" message.

By the way, you'll also notice that the *ngIf directive in the first <mat-error> element employs the little-used "else" statement. Yes, you can do that.

In this case, that whole "if" statement reads: "Show this element if the email already exists, otherwise, show the child contents of the element named otherErrorCheck."

That other element is an ng-template element.  By default child elements of <ng-template> are hidden.

However, they can be revealed with *ngIf directives like you see above.

Tryouts

Now let's see if this whole thing is working.

Launch the Spring Boot user service. Then, launch the Spring Boot contact service.

Next, run the Angular app. Go to http://localhost:4200/login and login with the usual credentials (darth/thedarkside).

Now, navigate to Contacts and Add Contact.

Fill out the form and create a brand new contact.

Then, navigate to the dashboard and back to Add Contact.

When you get to the part where you enter an email address, enter the same email address you entered before. Then, tab away to the next field.

You should see this:

 

So it works!

Now, navigate back to the email field and enter an invalid email address. For example, put a space in the middle of it.

You should see a different error message:

 

Excellent. It's responding as it should.

Wrapping It Up

Now it's over to you. Look for other places you might use asynchronous validators.

Also, find ways to refactor the code on both the server and client side.

Remember, you can always grab the source on GitHub. The server-side source is here and the client-side source is here.

Have fun!

Photo by Budgeron Bach from Pexels