I wanted to use template forms and [min]
and [max]
directives, so I have created them and they work. But the test confuses me: validation is not executed asynchronously, yet after changing my values and stuff, I have to go through this:
component.makeSomeChangeThatInvalidatesMyInput();
// control.invalid = false, expected
fixture.detectChanges();
// control.invalid is still false, not expected
// but if I go do this
fixture.whenStable().then(() => {
// control.invalid is STILL false, not expected
fixture.detectChanges();
// control.invalid now true
// expect(... .errors ... ) now passes
})
I don't understand why would I need even that whenStable()
, let alone another detectChanges()
cycle. What am I missing here? Why do I need 2 cycles of change detection for this validation to be executed?
Doesn't matter if I run the test as async
or not.
Here's my test:
@Component({
selector: 'test-cmp',
template: `<form>
<input [max]="maxValue" [(ngModel)]="numValue" name="numValue" #val="ngModel">
<span class="error" *ngIf="val.invalid">Errors there.</span>
</form>`
})
class TestMaxDirectiveComponent {
maxValue: number;
numValue: number;
}
fdescribe('ValidateMaxDirective', () => {
let fixture: ComponentFixture<TestMaxDirectiveComponent>;
let component: TestMaxDirectiveComponent;
beforeEach(async(() => TestBed.configureTestingModule({
imports: [FormsModule],
declarations: [TestMaxDirectiveComponent, ValidateMaxDirective],
}).compileComponents()
.then(() => {
fixture = TestBed.createComponent(TestMaxDirectiveComponent);
component = fixture.componentInstance;
return fixture.detectChanges();
})
));
fit('should have errors even when value is greater than maxValue', async(() => {
component.numValue = 42;
component.maxValue = 2;
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.error')).toBeTruthy();
});
}));
});
And here's the directive itself (simplified a bit):
const VALIDATE_MAX_PROVIDER = {
provide: NG_VALIDATORS, useExisting: forwardRef(() => ValidateMaxDirective), multi: true,
};
@Directive({
selector: '[max][ngModel]',
providers: [VALIDATE_MAX_PROVIDER],
})
export class ValidateMaxDirective implements Validator {
private _max: number | string;
@Input() get max(): number | string {
return this._max;
}
set max(value: number | string) {
this._max = value;
}
validate(control: AbstractControl): ValidationErrors | null {
if (isEmptyInputValue(control.value) || isEmptyInputValue(this._max)) {
return null; // don't validate empty values to allow optional controls
}
const value = parseFloat(control.value);
return !isNaN(value) && value > this._max ? {'max': {'max': this._max, 'actual': control.value}} : null;
}
}
I have tested this on a brand new ng new app
with @angular/cli
version 1.6.8 and latest angular 5.2.
The creating an async validator is very similar to the Sync validators. The only difference is that the async Validators must return the result of the validation as an observable (or as Promise). Angular does not provide any built-in async validators as in the case of sync validators. But building one is very simple.
The async validator's validate method returns a Promise that resolves if validation passes and value is updated or rejects with an Error if validation does not pass and value is not updated. The async validator also has a hint field that returns a Promise that when resolved will return a hint.
An asynchronous function is a function that operates asynchronously via the event loop, using an implicit Promise to return its result.
To add validation to a template-driven form, you add the same validation attributes as you would with native HTML form validation. Angular uses directives to match these attributes with validator functions in the framework.
After our conversation I've got it. You asked me what is async in the code above :
validate()
is !
we see that this method takes control: AbstractControl
as a parameter
in it's docs you'll find that as well as a synchronous behavior this handles asynchronous validation.
So I'm running on the assumption here that the adding of that parameter turned validate()
asynchronous.
this in turn means that you need to wait for it's eventual return
to assess whether there have been changes or not.
...This being the only function likely to trigger a change, we depend on it when we .detectChanges();
.
and in any async case in javascript values (variable) are to be imagined using the time dimension on top of whatever others they may already possess.
as such developers in the javascript community have adopted the "marbles on a string" or "birds on a telephone line" metaphors to help explain them.
the common theme being a lifeline/timeline. Here's another, my own personal representation :
you'll have to .subscribe()
or .then()
to have what you want executed executed at the time of hydration/return.
so when you :
component.makeSomeChangeThatInvalidatesMyInput(); // (1)
fixture.detectChanges(); // (2)
fixture.whenStable() // (3)
.then(() => { // (not a step) :we are now outside the
//logic of "order of execution" this code could happen much after.
fixture.detectChanges();
})
In step (2) you are effectively making that first assessment in my diagram above, the one straight onto the timeline where nothing has yet happened.
but in (not a step) you are listening for everytime there is a change (so potentially many calls). you get the expected value there at last because the code execution for assessing is happening "right on time" to catch the correct result; even better, it's happening because of the result(s).
detectChanges()
can detect the changes so assessing before you run detectChanges()
, even once within the .then()
, will return a premature value.the result that your first .detectChanges()
does not detect a change whereas your fixture.whenStable().then(() => {fixture.detectChanges()})
does is not a bug and is javascript functioning as intended.
(that includes jasmine, jasmine is pure javascript)
So there you have it! there was no odd behavior after all :)
hope this helps!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With