Tutorial

Unique Field Validation in Angular for Create and Update Operations

Published on February 27, 2018
Default avatar

By Spencer Feng

Unique Field Validation in Angular for Create and Update Operations

This tutorial is out of date and no longer maintained.

Introduction

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 that uses the same custom async validator to validate the uniqueness of the email field in a form that is responsible for creating and updating a customer record.

Note: 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
      })
    });
});

// ...

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.

Note: 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>

Conclusion

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.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about us


About the authors
Default avatar
Spencer Feng

author

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more
DigitalOcean Cloud Control Panel