This article builds upon my prior articles entitled From Zero to CRUD in Angular: Part 1 and From Zero to CRUD in Angular: Part 2. If you haven't already read these two articles, please go back and do so because this article adds to the project created in Part 2. In the last article, you learned to add, edit, and delete data via Web API calls. You also learned to handle validation errors coming back from the Entity Framework. In this article, you'll add additional server-side validation to the generated Entity Framework classes. You'll also learn to use the built-in client-side validation in Angular. Finally, you'll create your own custom Angular directive to validate data not supported by the built-in validation.
Additional Server-Side Validation
In the last article, you saw validation messages returned from the Data Annotations that the Entity Framework adds to your generated classes. However, there are often additional validation rules that you need to add to your classes that can't be done with Data Annotations or aren't automatically generated. To add additional validation, extend the ProductDB
class created by the Entity Framework.
Add a new class in the \Models
folder named ProductDB-Extension. After the file is added, rename the class inside of the file to ProductDB
and make it a partial class.
public partial class ProductDB
{
}
Adding this extension allows you to add additional functionality to the ProductDB Entity Framework model generated in Part 1 (May/June 2017) of this article series. When you attempt to insert or update data using the Entity Framework, it calls a method named ValidateEntity
to perform validation on any data annotations added to each property. Override this method to add your own custom validations. Add the following code to the ProductDB
class in the ProductDB-Extension.cs
file you just added. This method doesn't have any functionality yet. You'll add that soon.
protected override DbEntityValidationResult
ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items) {
return base.ValidateEntity(entityEntry, items);
}
Add a new method named ValidateProduct
just after the ValidateEntity
method you added. In this method, you add the custom validation rules. This method returns a list of DbValidationError
objects for each validation that fails.
protected List<DbValidationError>ValidateProduct(Product entity) {
List<DbValidationError> list =new List<DbValidationError>();
return list;
}
The ValidateEntity
method (Listing 1) is called once for each entity class in the model you're validating. In this example, you're only validating the Product
object because that's what the user has entered. The entityEntry
parameter passed into this method has an Entity
property that contains the current entity being validated. Write code to check to see whether that property is a Product
object. If it is, pass that object to the ValidateProduct
method. The ValidateProduct
method returns a list of additional DbValidationError
objects that need to be returned. If the list count is greater than zero, return a new DbEntityValidationResult
object by passing in the entityEntry
property and your new list of DbValidationError
objects.
Listing 1: The ValidateEntity method is called by the Entity Framework so you can add additional validations.
protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry,
IDictionary<object, object> items) {
List<DbValidationError> list = new List<DbValidationError>();
if (entityEntry.Entity is Product) {
Product entity = entityEntry.Entity as Product;
list = ValidateProduct(entity);
if (list.Count > 0) {
return new DbEntityValidationResult(entityEntry, list);
}
}
return base.ValidateEntity(entityEntry, items);
}
Now write the ValidateProduct
method to perform the various validations for your product data. Check any validations not covered by Data Annotations. The code in Listing 2 is the code for the ValidateProduct
method.
Listing 2: Write additional validation rules in the ValidateProduct method.
protected List<DbValidationError> ValidateProduct(Product entity)
{
List<DbValidationError> list = new List<DbValidationError>();
// Check ProductName field
if (string.IsNullOrEmpty(entity.ProductName)) {
list.Add(new DbValidationError("ProductName",
"Product Name must be filled in."));
}
else {
if (entity.ProductName.ToLower() == entity.ProductName) {
list.Add(new DbValidationError("ProductName",
"Product Name must not be all lower case."));
}
if (entity.ProductName.Length < 4 || entity.ProductName.Length > 150) {
list.Add(new DbValidationError("ProductName",
"Product Name must have between 4 and 150 characters."));
}
}
// Check IntroductionDate field
if (entity.IntroductionDate < DateTime.Now.AddYears(-5)) {
list.Add(new DbValidationError("IntroductionDate",
"Introduction date must be within the last five years."));
}
// Check Price field
if (entity.Price <= Convert.ToDecimal(0) || entity.Price > Convert.ToDecimal(9999.99)) {
list.Add(new DbValidationError("Price",
"Price must be greater than $0 and less than $10,000."));
}
// Check Url field
if (string.IsNullOrEmpty(entity.Url)) {list.Add(new DbValidationError("Url",
"Url must be filled in."));
}
else {
if (entity.Url.Length < 5 || entity.Url.Length > 255) {
list.Add(new DbValidationError("Url", "Url must be between
5 and 255 characters."));
}
}
return list;
}
The additional validation rules added to the product class are:
- The product name must not be all lower case
- The product name length must be between 4-150 characters
- The product introduction must be greater than five years ago
- The price must be between $0.01 and $9,999.99
- The URL length must between 5-255 characters
Run the application, click on the Add New Product button, and put in some data to make one or more of the rules in this method fail. For example, add a product name that's all lower case, don't add an introduction date, and add a price that has a value of -1. Click the Save button and you should see a screen that looks like Figure 1.
Client-Side Validation
Instead of having to perform a round trip to the server to check validation, Angular has great support for client-side validation. Add standard HTML attributes, such as required, pattern, minlength and maxlength to your input fields and Angular can perform the appropriate validation for you. Unfortunately, the HTML5 attributes min and max are not currently supported by Angular. You must write your own custom validation directive to support these. To use Angular validation, there are a few things you must do on your input page.
- Add <form #productForm=“ngForm”>.
- Add the name attribute to all input controls.
- Add a template variable to all input controls and assign to ngModel.
- Add error messages that appear when validation fails.
Let's add these things to the product-detail.component.html
file. At the top of this page, add the <form>
element. Then add the closing </form>
element tag at the bottom of this page.
<form #productForm="ngForm">
<div class="panel panel-primary" *ngIf="product">
// The rest of the HTML code
</div>
</form>
Next, you need to add the name attribute and a template variable to each input control within the <form>
tag. Also, add the appropriate validation attributes to each input field. Locate the product name input field and modify it to look like the following code snippet.
<input id="productName"
name="productName"
#productName="ngModel"
required
minlength="4"
maxlength="50"
type="text"
class="form-control"
autofocus="autofocus"
placeholder="Enter the Product Name"
title="Enter the Product Name"
[(ngModel)]="product.productName" />
Add the name attribute and set it to the same value as the ID attribute. Also add a template variable using the same exact name as well; in this case, #productName
. You always set the template variable to ngModel
. Adding the name attribute helps Angular associate this control with the controls in its internal model. Creating the template variable allows you to access the state of the control, such as whether that control is valid and has been touched.
Add the following attributes to the introduction date field.
name="introductionDate"
#introductionDate="ngModel"
required
Add the following attributes to the price field.
name="price"
#price="ngModel"
required
Add the following attributes to the URL field
name="url"
#url="ngModel"
Required
Add Validation Error Messages
Now that you've added the appropriate attributes to validate user input, you need error messages to display. In the last article, you added an un-ordered list as a place to display error messages. You're going to modify the *ngIf
to check to see if the productForm template variable has been touched and if it is valid or not. Go ahead and add this to the *ngIf
in the error message area.
<div class="row"
*ngIf="(messages && messages.length) ||
(productForm.form.touched &&
!productForm.form.valid)">
<div class="col-xs-12">
<div class="alert alert-warning">
<ul>
<li *ngFor="let msg of messages">
{{msg}}
</li>
</ul>
</div>
</div>
</div>
Just below the </li>
element in the above snippet, add the list items shown in Listing 3.
Listing 3: Display validation errors for input fields.
<li [hidden]="!productName.errors?.required">
Product Name is required
</li>
<li [hidden]="!productName.errors?.minlength">
Product Name must be at least 4 characters.
</li>
<li [hidden]="!productName.errors?.maxlength">
Product Name must be 150 characters or less.
</li>
<li [hidden]="!introductionDate.errors?.required">
Introduction Date is required
</li>
<li [hidden]="!price.errors?.required">
Price is required
</li>
<li [hidden]="!url.errors?.required">
URL is required
</li>
Each list item binds the hidden attribute to a Boolean value returned from the appropriate property on the errors object of each control. For example, on the product name field, you added the required attribute. To check to see if the error message “Product Name is required” is displayed, query the productName.errors?.required property. A question mark is used after the errors
property in case this object is null
. The errors
property is null
if there are no errors on that control.
Run the application and click on the Add New Product button. Click the Save button right away and you should see a couple of the validation messages appear. There was no round trip to the server; this all happened client-side through Angular validation. Delete any text in the URL field and you should immediately see an additional error message.
Click Cancel to go back to the product list page. This time, click on the Add New Product button, but don't hit the Save button. Delete any text in the URL field and tab off of that field. Now all error messages appear, as shown in Figure 2, because Angular detected that the page has been touched. Remember the line of code you added in the *ngIf
statement around the message area productForm.form.touched
? It's this line of code that suppresses error messages until you actually change something on the page. If you remove this bit of code, the error messages will appear right when you enter the detail page. It's your choice how you want your error messages to appear.
Custom Client-Side Validation
When you wrote code on the server-side, you included some additional rules. You made sure that the product name wasn't all lower case. You ensured that the price was greater than 0 and less than 10,000. This validation isn't built-in to HTML attributes, so you must create these rules yourself. Angular supplies a mechanism to do this custom validation through a Validator
class. Let's create three validator classes; one to check to ensure that a field isn't all lower case, one to check for a minimum value, and one to check for a maximum value.
Lower-Case Validator
Add new folder named shared
to the \src\app folder
. Using a shared
folder signifies that the classes contained in this folder are used across various parts of your Angular application. Add a new TypeScript
file named validator-notlowercase.directive.ts
within this shared folder. Write the code shown in Listing 4.
Listing 4: Add a Validator class to check if a string is not all lower case.
import { Directive, forwardRef }
from '@angular/core';
import { AbstractControl, Validator, NG_VALIDATORS, ValidatorFn }
from '@angular/forms';
function notLowerCaseValidate (c: AbstractControl): ValidatorFn {
let ret: any = null;
const value = c.value;
if (value) {
if (value.toString().trim() === value.toString().toLowerCase().trim()) {
ret = { validateNotlowercase: { value } };
}
}
return ret;
}
@Directive({
selector: '[validateNotlowercase]', providers: [{
provide: NG_VALIDATORS, useExisting: forwardRef(() =>
NotLowerCaseValidatorDirective), multi: true
}]
})
export class NotLowerCaseValidatorDirective implements Validator {
private validator: ValidatorFn;
constructor() {
this.validator = notLowerCaseValidate;
}
validate(c: AbstractControl): { [key: string]: any } {
return this.validator(c);
}
}
There are three key components to any custom validator you create with Angular. First, you need a function that does the actual work of validating the data entered. In this example, that's the function notLowerCaseValidate
. Second, you need a class to reference the validation function. In this example, that's the class NotLowerCaseValidatorDirective
. Third, is the selector name, which is the attribute you add to any input field. The selector name is defined in the selector
property in the @Directive
decorator function.
The notLowerCaseValidate
function is passed a reference to the input control upon which the selector is attached. First, you move the text in the value
property of the control into a constant named value
. This is more efficient than querying the text in the c.value
property each time you use it. After determining whether the text in the control has any value, you compare the value to the lower-case version of the value. If the value is all lower case, return an object with a property that's the name of the selector, and the value of this property is any set of values that you want to use. In this case, I'm just passing the value in the constant back. If the value isn't all lower case, you return a null
, which tells Angular that the value is valid.
The NotLowerCaseValidatorDirective
class itself is simple to understand. A single property named validator
is defined to be of the type ValidatorFn
. In the constructor, you assign a reference to the notLowerCaseValidate
function to the validator
property. This class implements the Validator
interface, so there's a validate()
function that's passed a reference to the input control to which the selector is attached.
The @Directive
decorator function is passed in two properties: selector
and providers
. The selector
property is self-explanatory: it's the name of the attribute you're going to add to the input field. The providers
property is an array of objects. The only object required for this property is one in which you specify three properties. The provider
property is almost always set to NG_VALIDATORS
. This tells Angular to register this class as a validation provider. The useExisting
property tells Angular to just create one instance of this class to be used for validations on this one control. The forwardRef()
is necessary because the @Directive
decorator function is running prior to getting to the class NotLowerCaseValidatorDirective
defined. Setting the multi
to a true
value means that this validation class may be used on more than one control within a form.
Now that you have the code written for this validation, you need to declare your intention to use it by modifying the AppModule
class. Open the app.module.ts
file and add the following import statement at the top of the file:
import { NotLowerCaseValidatorDirective }
from "./shared/validator-notlowercase.directive";
Add to the declarations
property within the @NgModule
decorator function this new directive:
declarations: [ ..., NotLowerCaseValidatorDirective],
It's now time to use this new validation. Open the product-detail.component.html
page and locate the productName
control. Add the new validateNotlowercase
attribute on this control, as shown in the following snippet:
<input id="productName"
name="productName"
#productName="ngModel"
validateNotlowercase
required
...
The last thing you need to do is find the location where you added all of your other validation error messages and add a new list item to display an error message if the product name is entered with all lower-case letters.
<li [hidden]="!productName.errors?.validateNotlowercase">
Product Name must not be all lower case
</li>
Run the application and click on the Add New Product button. Enter a product name in all lower case and tab off the field. You should see the validation message you wrote appear at the top of the panel.
Min Validator
For the price field on the product page, you should verify that the data input is greater than a specified amount. For this, build a minimum value validator. Add a TypeScript
file named validator-min.directive.ts
to the shared folder. Write the code shown in Listing 5.
Listing 5: Add a Validator class to check if a value is not below a minimum amount.
import { Directive, Input, forwardRef, OnInit }
from '@angular/core';
import { NG_VALIDATORS, Validator, ValidatorFn, AbstractControl }
from '@angular/forms';
export const min = (min: number): ValidatorFn => {
return (c: AbstractControl): { [key: string]: boolean } => {
let ret: any = null;
if (min !== undefined && min !== null) {
let value: number = +c.value;
if (value < +min) {
ret = { min: true };
}
}
return ret;
};
};
@Directive({
selector: '[min]', providers: [{
provide: NG_VALIDATORS, useExisting: forwardRef(() => MinValidatorDirective),
multi: true
}]
})
export class MinValidatorDirective implements Validator, OnInit {
@Input() min: number;
private validator: ValidatorFn;
ngOnInit() {
this.validator = min(this.min);
}
validate(c: AbstractControl): { [key: string]: any } {
return this.validator(c);
}
// The following code is if you want to change the value in the min="0.01"
// attribute thru code
// private onChange: () => void;
//
//ngOnChanges(changes: SimpleChanges) {
// for (let key in changes) {
// if (key === 'min') {
// this.validator = min(changes[key].currentValue);
// if (this.onChange) {
// this.onChange();
// }
// }
// }
//}
//registerOnValidatorChange(fn: () => void): void {
// this.onChange = fn;
//}
}
This code is somewhat different from the code you wrote for the lower-case validator. The reason for the changed code is that you're adding a value to the validator attribute attached to the price input field. This attribute looks like the following snippet.
[min]="0.01"
If you look at the export const min
declaration, you can see that this validator
function accepts a parameter named min. The value from the right-hand side of the attribute is passed to this parameter. Within this function, another function is returned that accepts an AbstractControl
as a parameter. Because this is an inner function to the min
function, it can use the min
parameter. Within this inner function, you compare the value in the passed-in control to the min
value set in the outer function.
Looking at the MinValidatorDirective
class, you can see that the @Directive
decorator function is almost exactly the same as the one for the lower-case validator. The only difference is, of course, the selector name. In the MinValidatorDirective
class, there's an @Input
variable. This is needed because the Angular form system passes in the value from the min
attribute on the control and assigns it to the @Input
variable with the same name.
In the ngOnInit
function, the value in the min
variable is passed to the exported constant
function called min
. The return value from this function is another function that accepts a control, so this function reference is given to the private variable name validator in this class. When a value is typed in by the user, the control is passed to the validate
function and the inner
function is run to determine if the value is greater than the value assigned to the min
parameter.
In Listing 5, notice there is some commented-out code. This code isn't necessary to do the standard validation that's required for the purposes of this article. This code is needed if you intend to change the value in the min attribute at runtime. If you do change the value, the ngOnChanges
event fires. You can then pick up this new change and reset the validator
property to a new instance of the function to call for validation.
Now that you understand how the min
validator works, open the app.module.ts
file and import this new directive, as shown in the following code snippet.
import { MinValidatorDirective }
from "./shared/validator-min.directive";
Next, add this new directive to the declarations
property in the @NgModule
decorator function.
declarations: [ ..., MinValidatorDirective],
Add the new min
attribute to the price field in the product-detail.component.html
. Open this file and add the min
attribute as shown below.
<input id="price"
name="price"
#price="ngModel"
[min]="0.01"
required
...
In the message area, add a new validation error message for the min
attribute. Within the unordered list, add a new list item.
<li [hidden]="!price.errors?.min">
Price must be greater than or equal to $0.01
</li>
Run the application and click on the Add New Product button. Enter a value of -1 in the price field and see the validation message you wrote appear at the top of the panel.
Max Validator
If you're checking a minimum value for the price field, you should also check for a maximum value. The code for this validator directive is almost the same as the min
validator other than using a greater-than sign instead of a less-than sign. Add a TypeScript file named validator-max.directive.ts
to the shared
folder. Write the code shown in Listing 6.
Listing 6: Add a Validator class to check if a value is not above a maximum amount.
import {Directive, Input, forwardRef, OnInit}
from '@angular/core';
import {NG_VALIDATORS, Validator, ValidatorFn, AbstractControl }
from '@angular/forms';
export const max = (max: number): ValidatorFn => {
return (c: AbstractControl): { [key: string]: boolean } => {
let ret: any = null;
if (max !== undefined && max !== null) {
let value: number = +c.value;
if (value > +max) {
ret = { max: true };
}
}
return ret;
};
};
@Directive({
selector: '[max]', providers: [{
provide: NG_VALIDATORS, useExisting: forwardRef(() => MaxValidatorDirective),
multi: true
}]
})
export class MaxValidatorDirective implements Validator, OnInit, OnChanges {
@Input() max: number;
private validator: ValidatorFn;
ngOnInit() {
this.validator = max(this.max);
}
validate(c: AbstractControl): { [key: string]: any } {
return this.validator(c);
}
}
Open the app.module.ts
file and import this new directive, as shown in the following code snippet.
import { MaxValidatorDirective }
from "./shared/validator-max.directive";
Next, add this new directive to the declarations
property in the @NgModule
decorator function.
declarations: [ ..., MaxValidatorDirective],
Add the new max
attribute to the price field in the product-detail.component.html
. Open this file and add the max attribute as shown here.
<input id="price"
name="price"
#price="ngModel"
required
[min]="0.01"
[max]="10000"
...
In the message area, add a new validation error message for the max
attribute. Within the unordered list, add a new list item.
<li [hidden]="!price.errors?.max">
Price must be less than or equal to $10,000
</li>
Run the application and click on the Add New Product button. Enter a value of 10001 in the price field and see the validation message you wrote appear at the top of the panel.
Summary
In this article, you added validation to your application. You added additional validation checks on the server as part of the Entity Framework. You then returned those messages back to the client and displayed them in the message area. Next, you learned how to use the built-in HTML5 attributes in combination with Angular validation to display error messages without making a round-trip. Finally, you learned to create your own custom validation directives to validate a string and a number. This concludes my series of articles on creating a CRUD page in Angular.