Skip to content

Commit 5ff715c

Browse files
edusperonidylhunn
authored andcommitted
fix(core): check if transplanted views are attached to change detector (#46974)
Prevents change detection on views transplanted in OnPush components that have been detached from change detection. PR Close #46974
1 parent 30c9e80 commit 5ff715c

File tree

2 files changed

+128
-6
lines changed

2 files changed

+128
-6
lines changed

packages/core/src/render3/instructions/shared.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1670,12 +1670,16 @@ function refreshContainsDirtyView(lView: LView) {
16701670
lContainer = getNextLContainer(lContainer)) {
16711671
for (let i = CONTAINER_HEADER_OFFSET; i < lContainer.length; i++) {
16721672
const embeddedLView = lContainer[i];
1673-
if (embeddedLView[FLAGS] & LViewFlags.RefreshTransplantedView) {
1674-
const embeddedTView = embeddedLView[TVIEW];
1675-
ngDevMode && assertDefined(embeddedTView, 'TView must be allocated');
1676-
refreshView(embeddedTView, embeddedLView, embeddedTView.template, embeddedLView[CONTEXT]!);
1677-
} else if (embeddedLView[TRANSPLANTED_VIEWS_TO_REFRESH] > 0) {
1678-
refreshContainsDirtyView(embeddedLView);
1673+
if (viewAttachedToChangeDetector(embeddedLView)) {
1674+
if (embeddedLView[FLAGS] & LViewFlags.RefreshTransplantedView) {
1675+
const embeddedTView = embeddedLView[TVIEW];
1676+
ngDevMode && assertDefined(embeddedTView, 'TView must be allocated');
1677+
refreshView(
1678+
embeddedTView, embeddedLView, embeddedTView.template, embeddedLView[CONTEXT]!);
1679+
1680+
} else if (embeddedLView[TRANSPLANTED_VIEWS_TO_REFRESH] > 0) {
1681+
refreshContainsDirtyView(embeddedLView);
1682+
}
16791683
}
16801684
}
16811685
}

packages/core/test/acceptance/change_detection_transplanted_view_spec.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,124 @@ describe('change detection for transplanted views', () => {
627627
viewRef.detectChanges();
628628
expect(component.checks).toEqual(1);
629629
});
630+
631+
describe('when detached', () => {
632+
@Component({
633+
selector: 'on-push-component',
634+
template: `
635+
<ng-container #vc></ng-container>
636+
`,
637+
changeDetection: ChangeDetectionStrategy.OnPush
638+
})
639+
class OnPushComponent {
640+
@ViewChild('vc', {read: ViewContainerRef}) viewContainer!: ViewContainerRef;
641+
@Input() template!: TemplateRef<{}>;
642+
643+
createTemplate() {
644+
return this.viewContainer.createEmbeddedView(this.template);
645+
}
646+
}
647+
648+
@Component({
649+
selector: 'check-always-component',
650+
template: `
651+
<ng-container #vc></ng-container>
652+
`,
653+
})
654+
class CheckAlwaysComponent {
655+
@ViewChild('vc', {read: ViewContainerRef}) viewContainer!: ViewContainerRef;
656+
@Input() template!: TemplateRef<{}>;
657+
658+
createTemplate() {
659+
return this.viewContainer.createEmbeddedView(this.template);
660+
}
661+
}
662+
let fixture: ComponentFixture<App>;
663+
let appComponent: App;
664+
let onPushComponent: OnPushComponent;
665+
let checkAlwaysComponent: CheckAlwaysComponent;
666+
667+
@Component({
668+
template: `
669+
<ng-template #transplantedTemplate>{{ incrementChecks() }}</ng-template>
670+
<on-push-component [template]="transplantedTemplate"></on-push-component>
671+
<check-always-component [template]="transplantedTemplate"></check-always-component>
672+
`
673+
})
674+
class App {
675+
@ViewChild(OnPushComponent) onPushComponent!: OnPushComponent;
676+
@ViewChild(CheckAlwaysComponent) checkAlwaysComponent!: CheckAlwaysComponent;
677+
transplantedViewRefreshCount = 0;
678+
incrementChecks() {
679+
this.transplantedViewRefreshCount++;
680+
}
681+
}
682+
beforeEach(() => {
683+
TestBed.configureTestingModule({declarations: [App, OnPushComponent, CheckAlwaysComponent]});
684+
fixture = TestBed.createComponent(App);
685+
fixture.detectChanges();
686+
appComponent = fixture.componentInstance;
687+
onPushComponent = appComponent.onPushComponent;
688+
checkAlwaysComponent = appComponent.checkAlwaysComponent;
689+
});
690+
describe('inside OnPush components', () => {
691+
it('should detect changes when attached', () => {
692+
onPushComponent.createTemplate();
693+
fixture.detectChanges(false);
694+
expect(appComponent.transplantedViewRefreshCount).toEqual(1);
695+
});
696+
697+
it('should not detect changes', () => {
698+
const viewRef = onPushComponent.createTemplate();
699+
viewRef.detach();
700+
fixture.detectChanges(false);
701+
expect(appComponent.transplantedViewRefreshCount).toEqual(0);
702+
viewRef.reattach();
703+
fixture.detectChanges(false);
704+
expect(appComponent.transplantedViewRefreshCount).toEqual(1);
705+
});
706+
707+
it('should not detect changes on mixed detached/attached refs', () => {
708+
onPushComponent.createTemplate();
709+
const viewRef = onPushComponent.createTemplate();
710+
viewRef.detach();
711+
fixture.detectChanges(false);
712+
expect(appComponent.transplantedViewRefreshCount).toEqual(1);
713+
viewRef.reattach();
714+
fixture.detectChanges(false);
715+
expect(appComponent.transplantedViewRefreshCount).toEqual(3);
716+
});
717+
});
718+
719+
describe('inside CheckAlways component', () => {
720+
it('should detect changes when attached', () => {
721+
checkAlwaysComponent.createTemplate();
722+
fixture.detectChanges(false);
723+
expect(appComponent.transplantedViewRefreshCount).toEqual(1);
724+
});
725+
726+
it('should not detect changes', () => {
727+
const viewRef = checkAlwaysComponent.createTemplate();
728+
viewRef.detach();
729+
fixture.detectChanges(false);
730+
expect(appComponent.transplantedViewRefreshCount).toEqual(0);
731+
viewRef.reattach();
732+
fixture.detectChanges(false);
733+
expect(appComponent.transplantedViewRefreshCount).toEqual(1);
734+
});
735+
736+
it('should not detect changes on mixed detached/attached refs', () => {
737+
checkAlwaysComponent.createTemplate();
738+
const viewRef = checkAlwaysComponent.createTemplate();
739+
viewRef.detach();
740+
fixture.detectChanges(false);
741+
expect(appComponent.transplantedViewRefreshCount).toEqual(1);
742+
viewRef.reattach();
743+
fixture.detectChanges(false);
744+
expect(appComponent.transplantedViewRefreshCount).toEqual(3);
745+
});
746+
});
747+
});
630748
});
631749

632750
function trim(text: string|null): string {

0 commit comments

Comments
 (0)