Unique Field Validation in Angular for Create and Update Operations

Spencer Feng

It’s best practice to use the same form for creating and editing a record in Angular. If there’s a field that should be unique, say an email field, we need to validate the uniqueness of the field with a backend API. When creating a new record, the unique field is valid as long as all records of the model do not have the same value for that field. However, when updating a record, the unique field is valid as long as the rest of the records do not have the same value for the field.

In this post, I will create a simple app which uses the same custom async validator to validate the uniqueness of the email field in a form which is responsible for creating and updating a customer record.

We already covered how to create custom async validators in this post. You may want to read that post first if this is new to you.

Backend API

Let’s start with the backend API that checks if there’s an existing customer record in the database. Here we use Express.js and MongoDB to build the API. As you can see, in this API endpoint, we use the value of customerId to determine if we are creating a customer or editing a customer.

// ...

router.post('/api/customers/checkEmailNotTaken', (req, res, next) => {
  const customerId = req.body.customerId;

  Customer.findOne({email: req.body.email})
    .then(customer => {
      // No customer with the same email in the database
      if (!customer) {
        return res.json({
          emailNotTaken: true
        });
      }

      // Validate the 'edit customer' form
      if (customerId) {
        if (customerId === customer._id.toString()) {
          return res.json({
            emailNotTaken: true
          })
        } else {
          return res.json({
            emailNotTaken: false
          })
        }
      }
      // Validate the 'create customer' form
      else {
        res.json({
          emailNotTaken: false
        })
      }
    })
    .catch(error => {
      res.json({
        emailNotTaken: true
      })
    });
});

// ...

Please note: if an error happens in the API call, the validation will pass and this will not be an issue, since in a real-world scenario, we will do a server-side validation for all form fields once the form is submitted.

Customer Service

Next, let’s create the Angular service that has a checkEmailNotTaken method which makes an HTTP POST request to the API endpoint we created in the previous step to validate the uniqueness of the email field.

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';

@Injectable()
export class CustomerService {
  constructor (private httpClient: HttpClient) {}

  // ...

  checkEmailNotTaken(email: string, customerId: string) {
    return this.httpClient.post('http://localhost:3001/api/customers/checkEmailNotTaken', {
      email,
      customerId
    });
  }

  // ...
}

Async Validator

Now, we’re going to create the async validator class which will use the checkEmailNotTaken method created in the Customer Service. We will define the async validator in its own file:

/validators/async-email-not-taken.validator.ts

import { AbstractControl } from '@angular/forms';
import { CustomerService } from '../customers/customer.service';
import 'rxjs/add/operator/map';

export class ValidateEmailNotTaken {
  static createValidator(customerService: CustomerService, customerId: string) {
    return (control: AbstractControl) => {
      return customerService.checkEmailNotTaken(control.value, customerId).map(res => {
        return res.emailNotTaken ? null : {emailTaken: true};
      });
    }
  }
}

Component

Our component initializes the reactive form which we use to create and edit a customer. In this component, we are trying to get the value of customerId through an observable. If we are creating a customer, customerId is an empty string; if we are editing a customer, customerId is the value passed in the URL.

We do not define the async validator for the email field when we create the form. Instead, we define the async validator for it after getting the value of customerId using the setAsyncValidators method of a reactive form control. At this point, our validator is able to pass the correct value of customerId to our API which will use it to determine if we are creating a customer or editing a customer.

In our app, the URL to the create a customer page is http://localhost/customers/create and the URL to the edit a customer page is http://localhost/customers/{customerId}/edit.

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Params } from '@angular/router';
import { Customer } from '../customer.model';
import { CustomerService } from '../customer.service';
import { ValidateEmailNotTaken } from '../../validators/async-email-not-taken.validator';

@Component({
  // ...
})
export class CustomerEditComponent implements OnInit {

  customerEditForm: FormGroup;
  customerId: string = '';
  editMode = false;
  pageTitle: string;
  buttonText: string;

  constructor(
    private fb: FormBuilder,
    private customerService: CustomerService,
    private route: ActivatedRoute
  ) {}

  // Getters
  get name() { return this.customerEditForm.get('name') }
  get email() { return this.customerEditForm.get('email') }

  ngOnInit() {
    this.createForm();

    this.route.params
      .subscribe(
        (params: Params) => {
          this.customerId = params['id'] ? params['id'] : '';
          this.editMode = params['id'] != null;

          this.initForm();

          this.pageTitle = this.editMode ? 'Edit Customer' : 'New Customer';
          this.buttonText = this.editMode ? 'Update' : 'Create';

          this.customerEditForm.controls['email'].setAsyncValidators(ValidateEmailNotTaken.createValidator(this.customerService, this.customerId));
        }
      )
  }

  createForm() {
    this.customerEditForm = this.fb.group({
      name: [
        '',
        Validators.required
      ],
      email: [
        '',
        [
          Validators.required,
          Validators.email
        ]
      ]
    });
  }

  onSubmit() {
    const customer: Customer = new Customer(
      '',
      this.customerEditForm.value.name,
      this.customerEditForm.value.email
    );

    if (!this.editMode) {
      this.customerService.createCustomer(customer)
        .subscribe(
          data => {
            this.customerEditForm.reset();
          },
          error => {
            console.error(error);
          }
        )
    } else {
      customer.id = this.customerId;
      this.customerService.updateCustomer(customer)
        .subscribe(
          data => {
            console.log(data);
          },
          error => {
            console.error(error);
          }
        )
    }
  }

  private initForm() {
    if (this.editMode) {
      this.customerService.getCustomer(this.customerId)
        .subscribe(
          data => {
            this.customerEditForm.setValue({
              name: data.name,
              email: data.email
            })
          },
          error => {
            console.error(error);
          }
        )
    }
  }

}

Template

Finally, here’s how our form’s markup can look like:

/customers/customer-edit/customer-edit.component.html

<form [formGroup]="customerEditForm" (ngSubmit)="onSubmit()">
  <div class="form-group">
    <label for="name">Name</label>
    <input
      type="text"
      class="form-control"
      id="name"
      formControlName="name">
    <div class="error" *ngIf="name.invalid && name.errors.required && (name.dirty || name.touched)">Please enter a name</div>
  </div>
  <div class="form-group">
    <label for="email">Email</label>
    <input
      type="eamil"
      class="form-control"
      id="email"
      formControlName="email">
    <div class="error" *ngIf="email.invalid && email.errors.required && (email.dirty || email.touched)">Please enter an email</div>
    <div class="error" *ngIf="email.invalid && email.errors.email && (email.dirty || email.touched)">Please enter a valid email</div>
    <div class="error" *ngIf="email.invalid && email.errors.emailTaken">This email has been taken, please use another one.</div>
  </div>
  <div class="form-group">
    <input type="submit" [value]="buttonText" class="btn btn-primary" [disabled]="customerEditForm.invalid">
  </div>
</form>

👉 And that's all there is to it! Now you're able to use the async validator class to validate a unique field in a form responsible for both creating and updating a record.

  Tweet It

🕵 Search Results

🔎 Searching...