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.

Figure 1: Server-Side validation rules fail and are reported back to the user.
Figure 1: Server-Side validation rules fail and are reported back to the user.

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.

Figure 2: Client-side validation is immediate with Angular.
Figure 2: Client-side validation is immediate with Angular.

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.