Like its predecessor, Angular 2 supports the declarative definition of validation rules. For instance, an application can use the minlength and maxlength attributes to enforce length constraints on fields:
<input [(ngModel)]="von" minlength="3" maxlength="30">
In this post, I demonstrate how to leverage this mechanism for custom validation rules. The example used here is also available in my demo project at [1].
Implementing a validation rule
In Angular 2, a validation rule is essentially a function that accepts a Control object and returns an error description object. This error object contains a property for each validation error detected. The application can later query these property keys to determine the outcome of the validation. If the validation function finds no issues, it returns an empty object.
A parameterizable validation rule can be represented as a higher-order function. This function takes the desired parameters and returns a function with the aforementioned characteristics.
The following example shows such a function, which checks the value range of a number:
import {Control} from 'angular2/common';
export class RangeValidator {
static create(min: number, max: number): Function {
return (c: Control): any => {
if (!c.value) return {};
var value = Number(c.value);
if (isNaN(value)) return {'range': true};
if (value >= min && value <= max) return {};
return {'range': true};
};
}
}
To provide a validation rule as an attribute, a directive must be set up. The example below illustrates the structure of such a directive. It receives a constructor argument for each parameter of the validation function. These arguments obtain their values from the desired attributes. The @Attribute decorator is used to specify the names of these attributes. With these values and the function shown above, the constructor creates the parameterized validation function. Additionally, the directive implements the Validator interface and its required validate method, which delegates to the validation function.
To determine which input fields the validation rule applies to, the directive is given a selector. Consequently, the directive is applied to input elements that have both the min and max attributes. For these elements, the directive binds itself to the NG_VALIDATORS token. Since each field can have multiple validation rules, a multi-binding is used for this purpose, as per the definition. This also nicely illustrates how Angular 2 internally processes declarative validations: it fetches all Validators bound to NG_VALIDATORS for each field via dependency injection and executes them to compute the validation result.
import {Directive, provide, Attribute} from 'angular2/core';
import {Control, NG_VALIDATORS, Validator} from 'angular2/common';
@Directive({
selector: 'input[min][max]',
providers: [provide(NG_VALIDATORS, {useExisting: RangeValidatorDirective, multi: true})]
})
export class RangeValidatorDirective implements Validator {
validator: Function;
constructor(@Attribute('min') min: number, @Attribute('max') max: number) {
this.validator = RangeValidator.create(min, max);
}
validate(c: Control) {
return this.validator(c);
}
}
To apply the validation rule, the developed directive must be registered with the component of choice:
@Component({
templateUrl: 'app/flug-edit/flug-edit.html',
directives: [RangeValidatorDirective]
})
export class FlugEdit {
[...]
}
Alternatively, the application can register the directive globally during bootstrapping. To do this, it associates the directive with the PLATFORM_DIRECTIVES token via a multi-binding:
import {PLATFORM_DIRECTIVES} from 'angular2/core';
[...]
var services = [
[...]
provide(PLATFORM_DIRECTIVES, {useValue: RangeValidatorDirective, multi: true})
];
bootstrap(App, services);
Afterward, the directive can be used in the template of the desired component. This requires only an element that matches the defined selector. In the case at hand, this is an input element with the min and max attributes.
To check whether the validation succeeded, the template uses the hasError method of the control. It passes the key that represents the validation error in the error description object. In this example, that key is range.
<form #f="ngForm">
<div class="form-group">
<label>Id: </label>
<input [(ngModel)]="flug.id" ngControl="id" class="form-control" min="0" max="9999">
<div *ngIf="f.controls.id?.hasError('range')">
Wert muss zwischen 0 und 9999 liegen!
</div>
</div>
</form>
[1] https://github.com/manfredsteyer/forms-sample-jm-2016-03
