fix(core): clear the `RefreshTransplantedView` when detached (#38768)

The `RefreshTransplantedView` flag is used to indicate that the view or one of its children
is transplanted and dirty, so it should still be refreshed as part of change detection.
This flag is set on the transplanted view itself as well setting a
counter on as its parents.
When a transplanted view is detached and still has this flag, it means
it got detached before it was refreshed. This can happen for "backwards
references" or transplanted views that are inserted at a location that
was already checked. In this case, we should decrement the parent
counters _and_ clear the flag on the detached view so it's not seen as
"transplanted" anymore (it is detached and has no parent counters to
adjust).

fixes #38619

PR Close #38768
This commit is contained in:
Andrew Scott 2020-09-09 10:49:35 -07:00 committed by Andrew Kushnir
parent db21c4fb44
commit d1415162cb
2 changed files with 36 additions and 1 deletions

View File

@ -300,6 +300,7 @@ function detachMovedView(declarationContainer: LContainer, lView: LView) {
// would be cleared and the counter decremented), we need to decrement the view counter here
// instead.
if (lView[FLAGS] & LViewFlags.RefreshTransplantedView) {
lView[FLAGS] &= ~LViewFlags.RefreshTransplantedView;
updateTransplantedViewCount(insertionLContainer, -1);
}

View File

@ -7,7 +7,7 @@
*/
import {CommonModule} from '@angular/common';
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DoCheck, Input, TemplateRef, Type, ViewChild} from '@angular/core';
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DoCheck, Input, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core';
import {AfterViewChecked} from '@angular/core/src/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';
@ -594,6 +594,40 @@ describe('change detection for transplanted views', () => {
'SheldonSheldonSheldon',
'Expected transplanted view to be refreshed even when insertion is not dirty');
});
it('should not fail when change detecting detached transplanted view', () => {
@Component({template: '<ng-template>{{incrementChecks()}}</ng-template>'})
class AppComponent {
@ViewChild(TemplateRef) templateRef!: TemplateRef<{}>;
constructor(readonly rootVref: ViewContainerRef, readonly cdr: ChangeDetectorRef) {}
checks = 0;
incrementChecks() {
this.checks++;
}
}
const fixture = TestBed.configureTestingModule({declarations: [AppComponent]})
.createComponent(AppComponent);
const component = fixture.componentInstance;
fixture.detectChanges();
const viewRef = component.templateRef.createEmbeddedView({});
// This `ViewContainerRef` is for the root view
component.rootVref.insert(viewRef);
// `detectChanges` on this `ChangeDetectorRef` will refresh this view and children, not the root
// view that has the transplanted `viewRef` inserted.
component.cdr.detectChanges();
// The template should not have been refreshed because it was inserted "above" the component so
// `detectChanges` will not refresh it.
expect(component.checks).toEqual(0);
// Detach view, manually call `detectChanges`, and verify the template was refreshed
component.rootVref.detach();
viewRef.detectChanges();
expect(component.checks).toEqual(1);
});
});
function trim(text: string|null): string {