Writing and Testing Custom Angular Validators: The 'Passwords Matching' Use Case
In this article, we'll teach you how to make the bane of modern man's existence: the 'password does not match' validation field.
Join the DZone community and get the full member experience.
Join For FreeImagine you are implementing a subscription form and you ask the user to type a password… and then to re-type it just to make sure. You may want to give the user some feedback if he typed a different password the second time, right?
One way of resolving that is to write a custom validator.
Validate This!
Our validator will be bound to a “Repeat Password” field and observe the original “Password” field: it will get the upper field’s value and compare it to its own (the lower one) to establish if the passwords match or not.
The form that renders such a situation might look a bit like this:
<form #myForm="ngForm">
...
<input name="password" #passwordModel="ngModel" required [(ngModel)]="password">
<input name="repeatPassword" #repeatPasswordModel="ngModel" required [fieldMatches]="passwordModel" [(ngModel)]="repeatPassword">"
...
<span [hidden]="!repeatPasswordModel?.errors?.fieldMatches">
The password typed in this field does not match the field above!
</span>
...
</form>
Nothing surprising here: we just make sure we have a reference to the “Password” field’s model because that’s what we will pass as a parameter to our custom validator, which we have named “fieldMatches.”
So how do we define that “fieldMatches” validator? We must declare a directive for that purpose, like this:
import {Directive, Input, OnChanges, SimpleChanges} from "@angular/core";
import {
AbstractControl, NG_VALIDATORS, NgModel, ValidationErrors, Validator, ValidatorFn,
Validators
} from "@angular/forms";
@Directive({
selector: '[fieldMatches]',
providers: [{
provide: NG_VALIDATORS,
useExisting: FieldMatchesValidatorDirective,
multi: true
}]
})
export class FieldMatchesValidatorDirective implements Validator, OnChanges {
@Input() fieldMatches: NgModel;
private validationFunction = Validators.nullValidator;
ngOnChanges(changes: SimpleChanges): void {
let change = changes['fieldMatches'];
if (change) {
const otherFieldModel = change.currentValue;
this.validationFunction = fieldMatchesValidator(otherFieldModel);
} else {
this.validationFunction = Validators.nullValidator;
}
}
validate(control: AbstractControl): ValidationErrors | any {
return this.validationFunction(control);
}
}
export function fieldMatchesValidator(otherFieldModel: NgModel): ValidatorFn {
return (control: AbstractControl): ValidationErrors => {
return control.value === otherFieldModel.value ? null : {'fieldMatches': {match: false}};
};
}
Let’s go through the sequence of events step by step:
- When the form field with fieldMatches is set up, a change event is fired and ngOnChanges() creates the validator function by using our homemade function factory, named fieldMatchesValidator().
- What is passed to fieldMatchesValidator() is the NgModel of the other field. This implies that in this implementation the change will be fired only once, essentially to generate our validation function!
- Then the user types something in the “Repeat Password” field, therefore the function assigned to this.validateFunction is called and receives the current AbstractControl.
- The function assigned to this.validationFunction, if the initialization took place, is the one containing our validation logic and initially provided by our function factory: fieldMatchesValidator().
- Our validation logic gets the AbstractControl object from which it can extract the field’s current value. It compares it with the value from the “Password” field’s model.
- If the fields are equal, null is returned. That means “all is well, mate!”
- If the fields are not equal, an object describing what is wrong is returned: ValidationErrors, which is basically an alias on a map. That object is added to the list of errors carried by the “Repeat Password” field’s NgModel.
- So now it is possible for your code to look at the “Repeat Password” NgModel and display something if its errors property carries a fieldMatches issue.
Validate the Validator
Now we could write isolated unit tests to test our validator, but these would only really be useful to verify our validation logic, which amounts to one line of code in this case. Or we could use Angular testing utilities to do something similar to an integration test, and see how our validator interacts with Angular and (most importantly) with a template.
We’ll go for the second option. For that we need two things:
- A fake template that will expose our validator.
- TestBed: a testing utility that allows us to set up an Angular testing module.
Brace yourself, there goes the code:
import {CommonModule} from "@angular/common";
import {FormsModule} from "@angular/forms";
import {Component} from "@angular/core";
import {async, ComponentFixture, ComponentFixtureAutoDetect, TestBed} from "@angular/core/testing";
import {By} from "@angular/platform-browser";
describe('FieldMatchesValidatorDirective', () => {
let component: TestComponent;
let fixture: ComponentFixture<TestComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [CommonModule, FormsModule],
declarations: [TestComponent],
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true },
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
});
it('should invalidate two fields that do not match', async(() => {
component.field1 = '12345678901234';
component.field2 = '12345678999999';
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let field2Model = fixture.debugElement.query(By.css('input[name=field2]')).references['field2Model'];
expect(field2Model.valid).toBe(false);
});
}));
});
@Component({
template: '<form #form1="ngForm">' +
'<input name="field1" #field1Model="ngModel" [(ngModel)]="field1">' +
'<input name="field2" #field2Model="ngModel" [fieldMatches]="field1Model" [(ngModel)]="field2">' +
'</form>'
})
class TestComponent {
field1: string;
field2: string;
}
The component at the very bottom (named… TestComponent, how original) is merely a component we set up for our test. It defines an in-line template, which declares our validator on one of two fields in a dummy form. It also exposes two properties that are bound to the dummy form’s fields.
That’s the component for which a fixture is returned when calling TestBed.createComponent().
When running, our test uses that fixture to set up the fields’ values using the fixture’s exposed component instance. It waits for all eventual pending asynchronous activities to end, fetches the second field’s reference to the model, from the fixture (always from the fixture, remember!), and tests if its validation status matches what we expect.
Conclusion
And essentially that’s all there is to it!
Now for a little trivia: if you implement the validator as described above, you will notice that in a specific situation you will not get the expected result. Which one, and why? I’ll let you ponder this.
This post is a shorter version of the one published on my blog. If you need a little more explanations or details feel free to check it here.
Until then,
Cheers!
Published at DZone with permission of Diego Pappalardo. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments