This tutorial is out of date and no longer maintained.
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.
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.
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
});
}
// ...
}
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:
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};
});
}
}
}
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);
}
)
}
}
}
Finally, here’s how our form’s markup can look like:
<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>
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.
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!
Sign up for Infrastructure as a Newsletter.
Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.