Integrating Validation in Angular 2 and ASP.NET Core
I’m building an Angular 2 app with an ASP.NET core web API behind it and need to add some validation. Obviously, I need to validate on the server in the web API, but I also want to do some client validation to make sure the user experience is good …
Server Validation
Here’s my model, which you can see is using attributes to define some required field validation:
public class Person
{
public int PersonId { get; set; }
[Required] public string Title { get; set; }
[Required] public string FirstName { get; set; }
[Required] public string Surname { get; set; }
[Required] public string EmailAddress { get; set; }
}
Here’s our controller below.
If the required fields aren’t filled in (detected from the attribute validation) a HTTP 400 status code is returned with the validation errors in the response body.
We also do some more complex validation, checking if the email address is already in use. Again we return a 400 and the validation error if the email address exists in our database.
[HttpPost]
public IActionResult Post([FromBody]Person person)
{
// return validation error if required fields aren't filled in
if (!ModelState.IsValid) { return BadRequest(ModelState); }
// return validation error if email already exists
Person existingPerson = peopleDataContext.People.Where(p => p.EmailAddress == person.EmailAddress).FirstOrDefault(); if (existingPerson != null) { ModelState.AddModelError("emailAddress", "This email already exists"); return BadRequest(ModelState); }
// validation has passed, so, save the person in the database
peopleDataContext.People.Add(person);
peopleDataContext.SaveChanges();
// returned the saved person to the client
return Json(person);
}
So, let’s test our API in Postman.
Test that required fields are validated:
Test that a valid person is saved:
Test that a person with an email address already in the database is validated:
Wiring up our Angular 2 app
Now, that we have our web API in place, let’s build our page to submit a new person.
At this point we’re not going to implement any clientside validation - we’re just going to handle the validation coming from the server.
Here’s our component markup:
<div class="alert alert-danger" role="alert" *ngIf="errors.length > 0">
<ul>
<li *ngFor="let error of errors">
{{ error }}
</li>
</ul>
</div>
<div class="alert alert-success" role="alert" *ngIf="successfulSave">
Person saved successfully!
</div>
<form
novalidate
[formGroup]="personForm"
(ngSubmit)="onSubmit()"
class="form-horizontal"
>
<div class="form-group">
<div class="col-sm-4">
<label class="control-label" for="title">Title</label><span>*</span>
<select id="title" class="form-control" formControlName="title">
<option></option>
<option>Ms</option>
<option>Mr</option>
<option>Mrs</option>
<option>Miss</option>
</select>
</div>
</div>
<div class="form-group">
<div class="col-sm-6">
<label class="control-label" for="firstName">First name</label
><span>*</span>
<input
type="text"
class="form-control"
id="firstName"
formControlName="firstName"
/>
</div>
</div>
<div class="form-group">
<div class="col-sm-6">
<label class="control-label" for="surname">Surname</label><span>*</span>
<input
type="text"
class="form-control"
id="surname"
formControlName="surname"
/>
</div>
</div>
<div class="form-group">
<div class="col-sm-6">
<label class="control-label" for="emailAddress">Email address</label
><span>*</span>
<input
type="email"
class="form-control"
id="emailAddress"
formControlName="emailAddress"
/>
</div>
</div>
<div class="form-group">
<div class="col-sm-3">
<button
type="submit"
class="btn btn-primary"
[disabled]="personForm.invalid"
>
Submit
</button>
</div>
</div>
</form>
- Lines 1-7 will show the validation errors from the server
- Lines 8-10 will show confirmation that the person has been saved into the database ok
- Line 12 defines our form, telling angular that the form object will be called
personForm
and submit handler will be a function calledonSubmit
- Lines 14-46 define our field inputs for the person
- Lines 48-52 defines our submit button
Here’s our component code:
import { Component, OnInit } from "@angular/core";
import {
FormBuilder,
FormGroup,
FormControl,
Validators
} from "@angular/forms";
import { Http, Response, Headers, RequestOptions } from "@angular/http";
import "rxjs/Rx";
@Component({
selector: "person",
template: require("./person.component.html")
})
export class PersonComponent implements OnInit {
personForm: FormGroup;
successfulSave: boolean;
errors: string[];
constructor(private fb: FormBuilder, private http: Http) {}
ngOnInit() {
this.personForm = this.fb.group({
title: [""],
firstName: [""],
surname: [""],
emailAddress: [""]
});
this.errors = [];
}
onSubmit() {
if (this.personForm.valid) {
let headers = new Headers({ "Content-Type": "application/json" });
let options = new RequestOptions({ headers: headers });
let person = {
title: this.personForm.value.title,
firstName: this.personForm.value.firstName,
surname: this.personForm.value.surname,
emailAddress: this.personForm.value.emailAddress
};
this.errors = [];
this.http
.post("/api/person", JSON.stringify(person), options)
.map(res => res.json())
.subscribe(
data => (this.successfulSave = true),
err => {
this.successfulSave = false;
if (err.status === 400) {
// handle validation error
let validationErrorDictionary = JSON.parse(err.text());
for (var fieldName in validationErrorDictionary) {
if (validationErrorDictionary.hasOwnProperty(fieldName)) {
this.errors.push(validationErrorDictionary[fieldName]);
}
}
} else {
this.errors.push("something went wrong!");
}
}
);
}
}
}
- Line 14 defines a variable called
errors
which will be an array of validation errors ngOnInit
on line 19 creates our form object with no client side validation defined at the momentonSubmit
posts the person object to our web API. You can see we catch validation errors on lines 47-55, pushing them to theerrors
array
If you run the app and hit the Submit button without filling in the inputs, you get something like:
Client Validation
Now, let’s implement the client validation in the angular app.
First, we need to change ngOnInit
to include the validation rules:
ngOnInit() {
this.personForm = this.fb.group({
title: ['', Validators.required], firstName: ['', Validators.required], surname: ['', Validators.required], emailAddress: ['', Validators.required] });
this.errors = [];
}
Then, we need to change our markup for the field inputs to output the validation errors. Here’s the markup for the title input:
<div
class="form-group"
[ngClass]="{'has-error':!personForm.controls.title.valid && personForm.controls.title.touched}">
<div class="col-sm-4">
<label class="control-label" for="title">Title</label><span>*</span>
<select id="title" class="form-control" formControlName="title">
<option></option>
<option>Ms</option>
<option>Mr</option>
<option>Mrs</option>
<option>Miss</option>
</select>
</div>
</div>
<div class="alert alert-danger" role="alert" *ngIf="!personForm.controls.title.valid && personForm.controls.title.touched"> You must enter a title</div>
- Line 1 adds the
has-error
CSS class if we have touched and not filled in the input - Lines 13-16 displays the error message beneath the input if we have touched and not filled in the input
So, cool, if the user doesn’t fill in any of the inputs, validation errors will be shown before the submit button is pressed:
However, what if we enter a duplicate email address:
As you can see, the validation error from the server is shown which is good, but ideally we want to highlight the email input and display the error beneath it. So, we need to integrate our validation errors from the server into Angular’s validation.
The solution is in the highlighted lines below. We know the field name from the server’s validation error dictionary. So, if the field is on our form, we flag to angular that there is an error using the control’s setErrors
function.
In our example, we don’t have any validation that is not bound to a single field (cross field validation), but if we did, the validation errors would be shown at the top of the page.
onSubmit() {
if (this.personForm.valid) {
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
let person = {
title: this.personForm.value.title,
firstName: this.personForm.value.firstName,
surname: this.personForm.value.surname,
emailAddress: this.personForm.value.emailAddress
};
this.errors = [];
this.http.post('/api/person', JSON.stringify(person), options)
.map(res => res.json())
.subscribe(
(data) => this.successfulSave = true,
(err) => {
this.successfulSave = false;
if (err.status === 400) {
// handle validation error
let validationErrorDictionary = JSON.parse(err.text());
for (var fieldName in validationErrorDictionary) {
if (validationErrorDictionary.hasOwnProperty(fieldName)) {
if (this.personForm.controls[fieldName]) { // integrate into angular's validation if we have field validation this.personForm.controls[fieldName].setErrors({ invalid: true }); } else { // if we have cross field validation then show the validation error at the top of the screen this.errors.push(validationErrorDictionary[fieldName]); } }
}
} else {
this.errors.push("something went wrong!");
}
});
}
}
Now, if we try to input a duplicate email, our user experience is much nicer:
If you to learn about using React with ASP.NET Core you might find my book useful: