fix(ivy): run pre-order hooks in injection order (#34026)

This commit fixes a compatibility bug where pre-order lifecycle
hooks (onInit, doCheck, OnChanges) for directives on the same
host node were executed based on the order the directives were
matched, rather than the order the directives were instantiated
(i.e. injection order).

This discrepancy can cause issues with forms, where it is common
to inject NgControl and try to extract its control property in
ngOnInit. As the NgControl directive is injected, it should be
instantiated before the control value accessor directive (and
thus its hooks should run first). This ensures that the NgControl
ngOnInit can set up the form control before the ngOnInit
for the control value accessor tries to access it.

Closes #32522

PR Close #34026
This commit is contained in:
Kara Erickson
2019-11-24 20:56:18 -08:00
committed by Matias Niemelä
parent caf5cffd53
commit ebe3229da5
11 changed files with 384 additions and 45 deletions

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Directive, EventEmitter, Input, Output, Type} from '@angular/core';
import {Component, Directive, EventEmitter, Input, Output, Type, ViewChild} from '@angular/core';
import {ComponentFixture, TestBed, async, fakeAsync, tick} from '@angular/core/testing';
import {AbstractControl, ControlValueAccessor, FormControl, FormGroup, FormsModule, NG_VALIDATORS, NG_VALUE_ACCESSOR, NgControl, NgForm, NgModel, ReactiveFormsModule, Validators} from '@angular/forms';
import {By} from '@angular/platform-browser/src/dom/debug/by';
@ -1055,6 +1055,16 @@ import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util'
fixture.detectChanges();
expect(fixture.componentInstance.control.status).toEqual('DISABLED');
});
it('should populate control in ngOnInit when injecting NgControl', () => {
const fixture = initTest(MyInputForm, MyInput);
fixture.componentInstance.form = new FormGroup({'login': new FormControl('aa')});
fixture.detectChanges();
expect(fixture.componentInstance.myInput !.control).toBeDefined();
expect(fixture.componentInstance.myInput !.control)
.toEqual(fixture.componentInstance.myInput !.controlDir.control);
});
});
describe('in template-driven forms', () => {
@ -1359,7 +1369,11 @@ export class MyInput implements ControlValueAccessor {
// TODO(issue/24571): remove '!'.
value !: string;
constructor(cd: NgControl) { cd.valueAccessor = this; }
control: AbstractControl|null = null;
constructor(public controlDir: NgControl) { controlDir.valueAccessor = this; }
ngOnInit() { this.control = this.controlDir.control; }
writeValue(value: any) { this.value = `!${value}!`; }
@ -1380,6 +1394,7 @@ export class MyInput implements ControlValueAccessor {
export class MyInputForm {
// TODO(issue/24571): remove '!'.
form !: FormGroup;
@ViewChild(MyInput) myInput: MyInput|null = null;
}
@Component({