fix(ivy): don't detect changes on detached child embedded views (#34846)
Fixes Ivy detecting changes inside child embedded views, even though they're detached. Note that there's on subtlety here: I made the changes inside `refreshDynamicEmbeddedViews` rather than `refreshView`, because we support detecting changes on a detached view (evidenced by a couple of unit tests), but only if it's triggered directly from the view's `ChangeDetectorRef`, however we shouldn't be detecting changes in the detached child view when something happens in the parent. Fixes #34816. PR Close #34846
This commit is contained in:
parent
ed24100976
commit
ef95da6d3b
|
@ -3,7 +3,7 @@
|
||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"runtime-es2015": 1485,
|
"runtime-es2015": 1485,
|
||||||
"main-es2015": 141476,
|
"main-es2015": 141985,
|
||||||
"polyfills-es2015": 36808
|
"polyfills-es2015": 36808
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"runtime-es2015": 1485,
|
"runtime-es2015": 1485,
|
||||||
"main-es2015": 147610,
|
"main-es2015": 148115,
|
||||||
"polyfills-es2015": 36808
|
"polyfills-es2015": 36808
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1525,7 +1525,10 @@ function refreshDynamicEmbeddedViews(lView: LView) {
|
||||||
const embeddedLView = viewOrContainer[i];
|
const embeddedLView = viewOrContainer[i];
|
||||||
const embeddedTView = embeddedLView[TVIEW];
|
const embeddedTView = embeddedLView[TVIEW];
|
||||||
ngDevMode && assertDefined(embeddedTView, 'TView must be allocated');
|
ngDevMode && assertDefined(embeddedTView, 'TView must be allocated');
|
||||||
refreshView(embeddedLView, embeddedTView, embeddedTView.template, embeddedLView[CONTEXT] !);
|
if (viewAttachedToChangeDetector(embeddedLView)) {
|
||||||
|
refreshView(
|
||||||
|
embeddedLView, embeddedTView, embeddedTView.template, embeddedLView[CONTEXT] !);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ((activeIndexFlag & ActiveIndexFlag.HAS_TRANSPLANTED_VIEWS) !== 0) {
|
if ((activeIndexFlag & ActiveIndexFlag.HAS_TRANSPLANTED_VIEWS) !== 0) {
|
||||||
// We should only CD moved views if the component where they were inserted does not match
|
// We should only CD moved views if the component where they were inserted does not match
|
||||||
|
|
|
@ -22,10 +22,10 @@ describe('change detection', () => {
|
||||||
@Directive({selector: '[viewManipulation]', exportAs: 'vm'})
|
@Directive({selector: '[viewManipulation]', exportAs: 'vm'})
|
||||||
class ViewManipulation {
|
class ViewManipulation {
|
||||||
constructor(
|
constructor(
|
||||||
private _tplRef: TemplateRef<{}>, private _vcRef: ViewContainerRef,
|
private _tplRef: TemplateRef<{}>, public vcRef: ViewContainerRef,
|
||||||
private _appRef: ApplicationRef) {}
|
private _appRef: ApplicationRef) {}
|
||||||
|
|
||||||
insertIntoVcRef() { this._vcRef.createEmbeddedView(this._tplRef); }
|
insertIntoVcRef() { return this.vcRef.createEmbeddedView(this._tplRef); }
|
||||||
|
|
||||||
insertIntoAppRef(): EmbeddedViewRef<{}> {
|
insertIntoAppRef(): EmbeddedViewRef<{}> {
|
||||||
const viewRef = this._tplRef.createEmbeddedView({});
|
const viewRef = this._tplRef.createEmbeddedView({});
|
||||||
|
@ -43,11 +43,8 @@ describe('change detection', () => {
|
||||||
class TestCmpt {
|
class TestCmpt {
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
TestBed.configureTestingModule({declarations: [TestCmpt, ViewManipulation]});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect changes for embedded views inserted through ViewContainerRef', () => {
|
it('should detect changes for embedded views inserted through ViewContainerRef', () => {
|
||||||
|
TestBed.configureTestingModule({declarations: [TestCmpt, ViewManipulation]});
|
||||||
const fixture = TestBed.createComponent(TestCmpt);
|
const fixture = TestBed.createComponent(TestCmpt);
|
||||||
const vm = fixture.debugElement.childNodes[0].references['vm'] as ViewManipulation;
|
const vm = fixture.debugElement.childNodes[0].references['vm'] as ViewManipulation;
|
||||||
|
|
||||||
|
@ -58,6 +55,7 @@ describe('change detection', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should detect changes for embedded views attached to ApplicationRef', () => {
|
it('should detect changes for embedded views attached to ApplicationRef', () => {
|
||||||
|
TestBed.configureTestingModule({declarations: [TestCmpt, ViewManipulation]});
|
||||||
const fixture = TestBed.createComponent(TestCmpt);
|
const fixture = TestBed.createComponent(TestCmpt);
|
||||||
const vm = fixture.debugElement.childNodes[0].references['vm'] as ViewManipulation;
|
const vm = fixture.debugElement.childNodes[0].references['vm'] as ViewManipulation;
|
||||||
|
|
||||||
|
@ -70,6 +68,109 @@ describe('change detection', () => {
|
||||||
expect(viewRef.rootNodes[0]).toHaveText('change-detected');
|
expect(viewRef.rootNodes[0]).toHaveText('change-detected');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not detect changes in child embedded views while they are detached', () => {
|
||||||
|
const counters = {componentView: 0, embeddedView: 0};
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<button (click)="noop()">Trigger change detection</button>
|
||||||
|
<div>{{increment('componentView')}}</div>
|
||||||
|
<ng-template #vm="vm" viewManipulation>{{increment('embeddedView')}}</ng-template>
|
||||||
|
`,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
class App {
|
||||||
|
increment(counter: 'componentView'|'embeddedView') { counters[counter]++; }
|
||||||
|
noop() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [App, ViewManipulation]});
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
const vm: ViewManipulation = fixture.debugElement.childNodes[2].references['vm'];
|
||||||
|
const button = fixture.nativeElement.querySelector('button');
|
||||||
|
const viewRef = vm.insertIntoVcRef();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(counters).toEqual({componentView: 1, embeddedView: 1});
|
||||||
|
|
||||||
|
button.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(counters).toEqual({componentView: 2, embeddedView: 2});
|
||||||
|
|
||||||
|
viewRef.detach();
|
||||||
|
button.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(counters).toEqual({componentView: 3, embeddedView: 2});
|
||||||
|
|
||||||
|
// Re-attach the view to ensure that the process can be reversed.
|
||||||
|
viewRef.reattach();
|
||||||
|
button.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(counters).toEqual({componentView: 4, embeddedView: 3});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not detect changes in child component views while they are detached', () => {
|
||||||
|
let counter = 0;
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: `<ng-template #vm="vm" viewManipulation></ng-template>`,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
class App {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<button (click)="noop()">Trigger change detection</button>
|
||||||
|
<div>{{increment()}}</div>
|
||||||
|
`,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
class DynamicComp {
|
||||||
|
increment() { counter++; }
|
||||||
|
noop() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to declare a module so that we can specify `entryComponents`
|
||||||
|
// for when the test is run against ViewEngine.
|
||||||
|
@NgModule({
|
||||||
|
declarations: [App, ViewManipulation, DynamicComp],
|
||||||
|
exports: [App, ViewManipulation, DynamicComp],
|
||||||
|
entryComponents: [DynamicComp]
|
||||||
|
})
|
||||||
|
class AppModule {
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({imports: [AppModule]});
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
const vm: ViewManipulation = fixture.debugElement.childNodes[0].references['vm'];
|
||||||
|
const factory = TestBed.get(ComponentFactoryResolver).resolveComponentFactory(DynamicComp);
|
||||||
|
const componentRef = vm.vcRef.createComponent(factory);
|
||||||
|
const button = fixture.nativeElement.querySelector('button');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(counter).toBe(1);
|
||||||
|
|
||||||
|
button.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(counter).toBe(2);
|
||||||
|
|
||||||
|
componentRef.changeDetectorRef.detach();
|
||||||
|
button.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(counter).toBe(2);
|
||||||
|
|
||||||
|
// Re-attach the change detector to ensure that the process can be reversed.
|
||||||
|
componentRef.changeDetectorRef.reattach();
|
||||||
|
button.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(counter).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('markForCheck', () => {
|
describe('markForCheck', () => {
|
||||||
|
|
Loading…
Reference in New Issue