DEV Community

Cover image for A generic Angular template-driven validator
Tim Deschryver for This is Angular

Posted on • Originally published at timdeschryver.dev

A generic Angular template-driven validator

Follow me on Twitter at @tim_deschryver | Subscribe to the Newsletter | Originally published on timdeschryver.dev.


A common complaint when it comes to creating custom validators with Angular template-driven forms is the boilerplate that one has to write before the validator can be used.

While I partly agree with that remark, it also has a clear separation of concerns because it adds a layer to validate the model.
For the complex models, I take the opportunity to use the validator in a way that the validator is acting as a separate layer, containing the business rules.

The part where I do agree is that you have to touch (and review) multiple files to create and register the directive.
The extra overhead (even if it's only a small effort) to add the validator directive to an Angular module is also often forgotten, which leads to frustrations.

But as we'll see, this doesn't have to be the case.
We can create one validator to rule them all.

Let's simply call this validator ValidatorDirective, and all this directive does is accept a callback to a method that returns the validation errors.

import { Directive, Input } from '@angular/core';
import { AbstractControl, ValidationErrors, NG_VALIDATORS, Validator } from '@angular/forms';

@Directive({
    selector: '[validator]',
    providers: [{ provide: NG_VALIDATORS, useExisting: ValidatorDirective, multi: true }]
})
export class ValidatorDirective implements Validator {
    @Input() validator: (control: AbstractControl) => ValidationErrors | null;

    validate(control: AbstractControl): ValidationErrors | null {
        return this.validator(control);
    }
}
Enter fullscreen mode Exit fullscreen mode

With the ValidatorDirective we can now validate a form while defining inline validators.
For example, to validate a single ngModel, we create the validator in the component.
The validator (scoreValidator) receives the control and returns the validation errors, just like the validate method from the Validator interface.

@Component()
export class Component {
    scoreValidator = (control: AbstractControl): ValidationErrors | null => {
        if (control.value > 10) {
            return { maxScore: 10 };
        }

        if (control.value < 0) {
            return { minScore: 0 };
        }

        return null;
    };
}
Enter fullscreen mode Exit fullscreen mode

To use the inline validator in the template, you assign the validator attribute (this is the selector from the ValidatorDirective directive) to the validate method (scoreValidator) .

<input type="number" name="score" ngModel [validator]="validateScore" />
Enter fullscreen mode Exit fullscreen mode

Instead of having to write all the validators manually, you can also invoke the built-in Angular validators, or invoke your custom validators.

@Component()
export class Component {
    scoreValidator = (control: AbstractControl): ValidationErrors | null => {
        // invoke multiple validators with `compose`
        // return Validators.compose([Validators.min(0), Validators.max(10)])(control);

        // it's also possible to short-circuit the validation
        return Validators.min(0)(control) || Validators.max(10)(control);
    };
}
Enter fullscreen mode Exit fullscreen mode

This is quick and easy for simple one-off validators.

Because validating a form group or even an entire form might get complex, it's a best practice to extract the validation rules outside of the component into its own method or class.
This keeps the component small and simple, and makes the validation logic easier to test.

To validate a ngModelGroup you can reuse the same validator directive.

<div ngModelGroup="person" [validator]="personValidator">
    <!-- imagine multiple form fields here -->
</div>
Enter fullscreen mode Exit fullscreen mode
@Component()
export class Component {
    personValidator = (control: AbstractControl): ValidationErrors | null => {
        return validatePerson(control.value);
    };
}
Enter fullscreen mode Exit fullscreen mode

Note that I'm using the arrow syntax while declaring these validators.
I do this to scope the method to the component class, instead of the directive class.
This allows me to use other class properties within the validator method.

Conclusion

We can eliminate some of the "boilerplate" 🙊 by creating one generic validator directive that accepts a callback to validate a form model.
This allows us to create inline validators within components. While this can be quick and easy for simple validations, I prefer to extract the complex validators into their own layer.

When the validation logic lives on its own (and not in a directive or in the component), it also doesn't bind the business rules to an Angular-specific layer.

The only downside to using the generic validator directive is that you can't revalidate the validator.

Demo


Follow me on Twitter at @tim_deschryver | Subscribe to the Newsletter | Originally published on timdeschryver.dev.

Top comments (0)