diff --git a/packages/core/src/render3/util/view_utils.ts b/packages/core/src/render3/util/view_utils.ts index 59f2656e7bae51..a7cb65b4279b89 100644 --- a/packages/core/src/render3/util/view_utils.ts +++ b/packages/core/src/render3/util/view_utils.ts @@ -206,6 +206,12 @@ export function requiresRefreshOrTraversal(lView: LView) { * parents above. */ export function updateAncestorTraversalFlagsOnAttach(lView: LView) { + // When we attach a view that's marked `Dirty`, we should ensure that it is reached during the + // next CD traversal so we add the `RefreshView` flag and mark ancestors accordingly. + if (lView[FLAGS] & LViewFlags.Dirty) { + lView[FLAGS] |= LViewFlags.RefreshView; + } + if (!requiresRefreshOrTraversal(lView)) { return; } diff --git a/packages/core/test/acceptance/change_detection_spec.ts b/packages/core/test/acceptance/change_detection_spec.ts index 77efadee3007c4..5262b9fb5321b5 100644 --- a/packages/core/test/acceptance/change_detection_spec.ts +++ b/packages/core/test/acceptance/change_detection_spec.ts @@ -187,14 +187,13 @@ describe('change detection', () => { @Component({ selector: `test-cmpt`, template: `{{counter}}|`, - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, }) class TestCmpt { counter = 0; @ViewChild('vc', {read: ViewContainerRef}) vcRef!: ViewContainerRef; - constructor() {} - createComponentView(cmptType: Type): ComponentRef { return this.vcRef.createComponent(cmptType); } @@ -202,18 +201,14 @@ describe('change detection', () => { @Component({ selector: 'dynamic-cmpt', - template: `dynamic`, + template: `dynamic|{{binding}}`, + standalone: true, changeDetection: ChangeDetectionStrategy.OnPush }) class DynamicCmpt { + @Input() binding = 'binding'; } - @NgModule({declarations: [DynamicCmpt]}) - class DynamicModule { - } - - TestBed.configureTestingModule({imports: [DynamicModule], declarations: [TestCmpt]}); - const fixture = TestBed.createComponent(TestCmpt); // initial CD to have query results @@ -221,20 +216,33 @@ describe('change detection', () => { fixture.detectChanges(false); expect(fixture.nativeElement).toHaveText('0|'); - // insert a dynamic component + // insert a dynamic component, but do not specifically mark parent dirty + // (dynamic components with OnPush flag are created with the `Dirty` flag) const dynamicCmptRef = fixture.componentInstance.createComponentView(DynamicCmpt); fixture.detectChanges(false); - expect(fixture.nativeElement).toHaveText('0|dynamic'); + expect(fixture.nativeElement).toHaveText('0|dynamic|binding'); // update model in the OnPush component - should not update UI fixture.componentInstance.counter = 1; fixture.detectChanges(false); - expect(fixture.nativeElement).toHaveText('0|dynamic'); + expect(fixture.nativeElement).toHaveText('0|dynamic|binding'); // now mark the dynamically inserted component as dirty dynamicCmptRef.changeDetectorRef.markForCheck(); fixture.detectChanges(false); - expect(fixture.nativeElement).toHaveText('1|dynamic'); + expect(fixture.nativeElement).toHaveText('1|dynamic|binding'); + + // Update, mark for check, and detach before change detection, should not update + dynamicCmptRef.setInput('binding', 'updatedBinding'); + dynamicCmptRef.changeDetectorRef.markForCheck(); + dynamicCmptRef.changeDetectorRef.detach(); + fixture.detectChanges(false); + expect(fixture.nativeElement).toHaveText('1|dynamic|binding'); + + // reattaching and run CD from the top should update + dynamicCmptRef.changeDetectorRef.reattach(); + fixture.detectChanges(false); + expect(fixture.nativeElement).toHaveText('1|dynamic|updatedBinding'); }); it('should support re-enterant change detection', () => {