The Working Programmer - How To Be MEAN: Validating Angular

The Working Programmer - How To Be MEAN: Validating Angular

By Ted Neward | March 2018

Ted NewardWelcome back again, MEANers.

In the previous column, I started looking at Angular’s support for forms and input. Two-way binding took top billing, but any sense of how to validate input—to make sure that Speakers have both a first and a last name, for example, and not just empty strings—was left behind. In this month’s column, we need to rectify that, because data input without validation is basically just asking users to pour garbage into your system, leaving it to you to sort out.

And that, as most of us know, would be bad. Like, trademarked levels of Really, Really Bad™.

SpeakerUI, Redux

In the last column, I ditched the SpeakerEdit component in favor of SpeakerUI, which wraps around a Speaker model instance and knows—based on what’s passed into it—whether it’s in a read-only state for viewing the Speaker, or an editable state. It uses two <div> sections (one of which is hidden depending on the state the component is in) to keep the UI distinct (see Figure 1). The read-only section requires no validation, of course, because there’s no user input; it’s the editable section that concerns us here.

Figure 1 The SpeakerUI Component
JavaScript
Copy
<form #speakerForm="ngForm">   <div [hidden]="readonly">     FirstName: <input name="firstName" type="text"       [(ngModel)]="model.firstName"><br>     LastName:  <input name="lastName" type="text"       [(ngModel)]="model.lastName"><br>       Subjects: {{model.subjects}}<br>     <button (click)="save()"       [disabled]="!speakerForm.form.dirty">Save</button>     <button (click)="cancel()"       [disabled]="!cancellable(speakerForm)">Cancel</button>     <br><span>{{diagnostic}}</span>   </div>   ... </form>

The first thing to note is that between last month’s column and this, I moved the logic for determining whether or not the edit mode can be canceled (notice how the Cancel button’s disabled property is bound) to a method on the component itself. This may be a bit of overkill in this particular case, but it does demonstrate an important aspect of Angular—that you do not have to do all of the UI logic directly inside the template itself. Should the cancellation logic get complicated, having that in the template is probably a bad idea, but we need to have the form object (the speakerForm object defined last month) available to use in the component code.

That requires the use of a new module, one that isn’t already present in the component: NgForm.

NgForm

NgForm is a class defined in Angular specifically for working with forms. It’s contained in a separate module from the rest of the Angular core, so it requires a standalone import to retrieve:

JavaScript
Copy
import { NgForm } from "@angular/forms";

When working with forms at runtime, Angular constructs a collection of objects that represents the various controls and the form itself, and uses it to do validation and other processing. This object collection is often hidden behind the scenes for convenience, but is always available to Angular developers for use.

Once passed into the cancellable method, you can use the form object to examine the state of the form via a number of properties. NgForm defines dirty, invalid, pristine, touched, untouched and valid properties to represent an entire spectrum of different user-­interaction states with the form. For demonstration purposes, I’ll add a few more diagnostic lines to the editable section of the form:

JavaScript
Copy
<br>Pristine: {{speakerForm.form.pristine}} Dirty: {{speakerForm.form.dirty}} Touched: {{speakerForm.form.touched}} Untouched: {{speakerForm.form.untouched}} Invalid: {{speakerForm.form.invalid}} Valid: {{speakerForm.form.valid}}

These will simply display the state of each of these as the user interacts with the form, and help explain what each represents. For example, “untouched” means—quite literally—the user hasn’t touched the form in any way. Simply clicking (or touching, on a mobile device) the edit field so that the cursor appears there is enough to render the form as being “touched.” However, if no typing has taken place, even if the form is “touched,” it’s still “pristine.” And so on.

Validity, as might be expected, suggests that the user has violated some kind of data-entry constraint that the developer has mandated. Angular looks to build off of standard HTML5 validity constraints, so, for example, if you decide that speakers must have both a first and a last name, you can simply use the “required” attribute on the edit fields:

JavaScript
Copy
FirstName: <input name="firstName" type="text"   [(ngModel)]="model.firstName" required><br> LastName:  <input name="lastName" type="text"   [(ngModel)]="model.lastName" required><br>

Given this, if the user edits an existing Speaker and clears either the firstName or lastName edit field completely, the invalid state flips to true and the valid state to false, because Angular recognizes that the required flag is present. That said, though, Angular doesn’t do anything else—out of the box, Angular doesn’t provide any built-in UI to indicate that the form is invalid. It’s up to the developer to signal to the user in some way that the field requires attention. This can be done in a variety of ways, all dependent on what the developer is using for UI support. For example, it’s common when using the Bootstrap CSS framework to flag the form field as requiring attention by coloring it (or some portion of it) red. Alternatively, it’s not uncommon to have a hidden text span below or after the field that will display when the constraints are violated in some way, and tie the span’s hidden attribute to the status of the form.

But that raises a subtle point—you would prefer to know which control within the form is invalid, so that you can tie the feedback directly to that control. Fortunately, the NgForm has a controls property, which is an array of NgControl objects, and each control defined within the form (such as firstName and lastName) will have an NgControl instance to represent it. Thus, you can reference those control objects directly within the hidden attribute’s template expression:

JavaScript
Copy
FirstName: <input name="firstName" type="text"   [(ngModel)]="model.firstName" required> <span [hidden]="speakerForm.controls.firstName.valid">   Speakers must have a first name</span><br> LastName:  <input name="lastName" type="text"   [(ngModel)]="model.lastName" required> <span [hidden]="speakerForm.controls.firstName.valid">   Speakers must have a first name</span><br>

Candor compels me to admit that this code has a subtle issue—­when run, it will yield a few errors at runtime. That’s because during the earliest stages of the component the NgForm hasn’t con­structed the collection of NgControl objects, and so the expression, speakerForm.controls.firstName, will be undefined.

The easy way to avoid this problem is to define a local template variable for the control, rather than go through the form’s controls array, and use *ngIf directives to test to see if the form is touched or dirty, and if so, whether it’s valid:

JavaScript
Copy
FirstName: <input name="firstName" type="text"   [(ngModel)]="model.firstName" #firstName="ngModel"   required> <div *ngIf="firstName.invalid &&             (firstName.dirty || firstName.touched)">   <div *ngIf="firstName.errors.required">     A first name is required.   </div> </div>

Essentially, this eliminates the need to work through the speakerForm, but it’s useful to know that the speakerForm object is accessible to us at runtime, albeit with some caveats.

Custom Validation

In those situations where the HTML5 standard doesn’t define a validation you want or need, Angular permits you to write a custom validator that can be invoked to test the field in question. For example, many years ago, let’s assume I had a bad experience with a speaker named Josh. I don’t like Josh. Never did. I don’t care to let the guy be a part of our database, so I want a custom form validator that disallows particular input. (Obviously, this is a pretty pedantic example, but the concepts here hold for just about any kind of validator that could be imagined.)

Like the other validation, Angular wants to try and “tap in” to the HTML syntax as much as possible, which means that even custom validators will appear like HTML5 validators, so the forbiddenName validator should appear like any other HTML validation rule:

JavaScript
Copy
FirstName: <input name="firstName" type="text"   [(ngModel)]="model.firstName" #firstName="ngModel"   required forbiddenName="josh">

Within the template, you can simply add the necessary *ngIf directive to test whether the form contains the forbidden name specified, and if so, display a specific error message:

JavaScript
Copy
<div *ngIf="firstName.invalid &&             (firstName.dirty || firstName.touched)">   <div *ngIf="firstName.errors.required">     A first name is required.   </div>   <div *ngIf="firstName.errors.forbiddenName">     NO JOSH!   </div> </div>

There. That should keep out any unwanted speakers (looking at you, Josh).

Custom Validator Directives

In order to make this work, Angular requires us to write the validator as an Angular directive, which is a means for hooking up some Angular code to an HTML-looking syntactic element, such as the forbiddenName directive in the input field, or the required or even *ngIf. Directives are quite powerful, and quite beyond the room I have here to explore fully. But I can at least explain how validators work, so let’s start by creating a new directive using “ng generate directive ForbiddenValidator” at the command line, and have it scaffold out forbidden-validator.directive.ts:

JavaScript
Copy
import { Directive } from "@angular/core"; @Directive({   selector: "[appForbiddenValidator]" }) export class ForbiddenValidatorDirective {   constructor() { } }

The selector in the @Directive is the syntax that you want to use in the HTML templates and, frankly, appForbiddenValidator doesn’t really get the heart racing. It should be something a little more clear in its usage, like forbiddenName. Additionally, the Directive needs to tap into the existing collection of validators—without going into too much detail, the providers parameter to the @Directive contains necessary boilerplate to make the ForbiddenValidatorDirective available to the larger collection of validators:

JavaScript
Copy
@Directive({   selector: "[forbiddenName]",   providers: [{provide: NG_VALIDATORS,                useExisting: ForbiddenValidatorDirective,                multi: true}] })

Next, the directive needs to implement the Validator interface, which provides a single method, validate, which—as could be guessed—is invoked when validation needs to be done. However, the result from the validate function isn’t some kind of pass/fail result of the validation, but the function to use to perform the validation itself:

JavaScript
Copy
export class ForbiddenValidatorDirective implements Validator {   @Input() forbiddenName: string;   validate(control: AbstractControl): {[key: string]: any} {     if (this.forbiddenName) {       const nameRe = new RegExp(this.forbiddenName, "i");       const forbidden = nameRe.test(control.value);       return forbidden ? {"forbiddenName": {value: control.value}} : null;     } else {       return null;     }   } }

Fundamentally, Angular walks through a list of validators, and if all of them return null, then everything is kosher and all the input is considered valid. If any of them return anything other than that, it’s considered to be part of the set of validation errors, and added to the errors collection that the template referenced in the *ngIf statements earlier.

If the validator has a forbiddenName value, which is what’s passed in from the directive’s usage, then there’s validation to be done—the component’s input is used to construct a RegExp instance (using “i” for a case-insensitive match), and then the RegExp test method is used to check to see if the control’s value matches the name-which-shall-not-be-accepted. If it does, then a set is constructed with the key forbiddenName (which is what the template was using earlier to determine whether to show the error message, remember), and the value being the control’s input. Otherwise, the directive hands back null, and the rest of the form’s validators are fired. If all of them hand back null, everything’s legit.

Wrapping Up

Angular’s validation support is, as you can tell, pretty extensive and full-featured. It builds off of the standard HTML validation support that’s found in every HTML5 browser, but provides a degree of runtime control support that’s extensible and powerful when needed. Most applications will find the built-in validators to be sufficient for most purposes, but having the ability to create custom validators means Angular can be as complex as necessary when working with user input. (Which is good, because users constantly find new ways to attempt to enter garbage into the system. I blame Josh for that.) There are still some more surprises hiding within the “@angular/forms” module before we’re done here, so stay tuned.

Happy coding!

Ted Neward is a Seattle-based polytechnology consultant, speaker, and mentor, currently working as the director of Developer Relations at Smartsheet.com. He has written a ton of articles, authored and co-authored a dozen books, and speaks all over the world. Reach him at [email protected] or read his blog at blogs.tedneward.com.

Thanks to the following Microsoft technical expert: Garvice Eakins

Nguồn: msdn.microsoft.com