This article was written against Angular 2 Beta 0. Future releases may introduce breaking changes.

When validating user input in Angular 2, developers typically attach validators to individual Controls. However, there are cases where validation must consider multiple fields at once. In such scenarios, it is possible to register the validator on the ControlGroup itself. This approach gives the validator full visibility into every Control belonging to that group.

The example below demonstrates this concept through a static method named validate. The method accepts a ControlGroup as its argument and looks for the fields von and nach within that group. If these fields are not yet present, the validation exits early and reports no errors. When the fields do exist, the method checks whether they hold valid values. In the event of a failure, it returns an error descriptor object containing the value route: true. Otherwise, it reports success by returning an empty object.

export class RouteValidator {

    static validate(group: ControlGroup): any {

        var von = group.find('von');
        var nach = group.find('nach');

        if (!von || !nach) return {};

        if (von.value == 'Graz' && nach.value == 'Hamburg') {
            return {};
        }
        return {
            route: true
        }        
    }

}

Working with the Imperative Forms API

When building forms imperatively, the ControlGroup instance is readily at hand. Its validator property accepts a validator function, as shown in the following snippet. For cases where multiple validators need to be applied to the same group, the Validators.compose method can combine them into a single, unified validator that is then assigned to the mentioned property.

@Component({
    selector: 'flug-suchen',
    templateUrl: 'app/flug-suchen2/flug-suchen2.html',
    directives: [CORE_DIRECTIVES, FORM_DIRECTIVES],
    pipes: [OrtPipe]
})
export class FlugSuchen2 {

    fluege = [];
    selectedFlug;
    flugService: FlugService;
    filter: ControlGroup;

    constructor(flugService: FlugService, fb: FormBuilder) {
        this.flugService = flugService;

        this.filter = fb.group({
            von: ['Graz'],
            nach: ['Hamburg'],
            maxSegmente: ['2']
        });

        this.filter.validator = RouteValidator.validate;

    }

    [...]
}

As usual, the view binds its ControlGroup via [ngFormModel]. Because the validator targets the entire ControlGroup, the view must also query for any errors at this group level. In the example that follows, this is achieved by invoking the filder.hasError method.

<form [ngFormModel]="filter">

    <div *ngIf="filter.hasError('route')">
        Diese Route wird nicht angeboten!
    </div>

    <div class="form-group">
        <label>Von</label>
        <input ngControl="von" class="form-control">
    </div>

    <div class="form-group">
        <label>Nach</label>
        <input ngControl="nach" class="form-control">
    </div>

    <div class="form-group">
        <label>Max. Segmente</label>
        <input ngControl="maxSegmente" class="form-control">
    </div>

    [...]

</form>

Working with the Declarative Forms API

To leverage custom group validators in declarative forms, a dedicated directive must be created. This directive also requires a validate method, which accepts a ControlGroup and performs the validation logic. The location of this method is entirely up to the developer—the directive only needs a reference to it. For the sake of brevity, the upcoming example places the validate method straight inside the directive class itself.

To connect the directive with the validate method, a provider is set up. This provider binds the directive class to the predefined Angular 2 token NG_VALIDATORS using a multi-provider registration. The directive's selector dictates how it is used in the view. In this particular case, the selector targets any element that carries the route attribute.

@Directive({
    selector: '[route]', 
    providers: [provide(NG_VALIDATORS, {useExisting: RouteValidatorDirective, multi: true})]
})
export class RouteValidatorDirective {

    validate(group: ControlGroup) {
        return RouteValidator.validate(group);
    }
} 

Angular 2 offers the Validator interface as an alternative signature for the validate method. However, that interface expects to validate a single Control, and a ControlGroup is not type-compatible with it, so it cannot be used in this context. Since interfaces are erased at compile time and Angular 2 has no runtime knowledge of them, this limitation has no practical consequences.

For the directive to be available in the desired view, it must be registered in the usual way—for instance, via the directives property of the Component decorator.

@Component({ 
    selector: 'flug-suchen',
    templateUrl: 'app/flug-suchen/flug-suchen.html',
    directives: [
        RouteValidatorDirective
    ]
})
export class FlugSuchen {
    [...]
}

Next, the view marks the form to be validated with the directive by adding the route attribute to the appropriate element. Once this is done, the view can check for a potential error on the ControlGroup that the FormDirective creates for the entire form. This error is accessed through the form property:

<form #f="ngForm" route>

    <div *ngIf="f.form.hasError('route')">
        Diese Route wird nicht angeboten!
    </div>

    <div class="form-group">
        <label>Von</label>
        <input [(ngModel)]="von" name="von" ngControl="von" ort required class="form-control">
    </div>

    <div class="form-group">
        <label>Nach</label>
        <input [(ngModel)]="nach" ortAsync name="nach" ngControl="nach" class="form-control">
    </div>

    <div class="form-group">
        <label>Max. Segmente</label>
        <input [(ngModel)]="maxSegmente" ngControl="maxSegmente" class="form-control" min="1" max="5">
    </div>

    [...]

</form>