Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 24 additions & 20 deletions packages/core/src/render3/instructions/change_detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,26 +46,7 @@ export function detectChangesInternal<T>(

try {
refreshView(tView, lView, tView.template, context);
let retries = 0;
// If after running change detection, this view still needs to be refreshed or there are
// descendants views that need to be refreshed due to re-dirtying during the change detection
// run, detect changes on the view again. We run change detection in `Targeted` mode to only
// refresh views with the `RefreshView` flag.
while (lView[FLAGS] & (LViewFlags.RefreshView | LViewFlags.HasChildViewsToRefresh) ||
lView[REACTIVE_TEMPLATE_CONSUMER]?.dirty) {
if (retries === MAXIMUM_REFRESH_RERUNS) {
throw new RuntimeError(
RuntimeErrorCode.INFINITE_CHANGE_DETECTION,
ngDevMode &&
'Infinite change detection while trying to refresh views. ' +
'There may be components which each cause the other to require a refresh, ' +
'causing an infinite loop.');
}
retries++;
// Even if this view is detached, we still detect changes in targeted mode because this was
// the root of the change detection run.
detectChangesInView(lView, ChangeDetectionMode.Targeted);
}
detectChangesInViewWhileDirty(lView);
} catch (error) {
if (notifyErrorHandler) {
handleError(lView, error);
Expand All @@ -85,6 +66,29 @@ export function detectChangesInternal<T>(
}
}

function detectChangesInViewWhileDirty(lView: LView) {
let retries = 0;
// If after running change detection, this view still needs to be refreshed or there are
// descendants views that need to be refreshed due to re-dirtying during the change detection
// run, detect changes on the view again. We run change detection in `Targeted` mode to only
// refresh views with the `RefreshView` flag.
while (lView[FLAGS] & (LViewFlags.RefreshView | LViewFlags.HasChildViewsToRefresh) ||
lView[REACTIVE_TEMPLATE_CONSUMER]?.dirty) {
if (retries === MAXIMUM_REFRESH_RERUNS) {
throw new RuntimeError(
RuntimeErrorCode.INFINITE_CHANGE_DETECTION,
ngDevMode &&
'Infinite change detection while trying to refresh views. ' +
'There may be components which each cause the other to require a refresh, ' +
'causing an infinite loop.');
}
retries++;
// Even if this view is detached, we still detect changes in targeted mode because this was
// the root of the change detection run.
detectChangesInView(lView, ChangeDetectionMode.Targeted);
}
}

export function checkNoChangesInternal<T>(
tView: TView, lView: LView, context: T, notifyErrorHandler = true) {
setIsInCheckNoChangesMode(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
*/

import {NgFor, NgIf} from '@angular/common';
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, Directive, inject, Input, signal, ViewChild} from '@angular/core';
import {PLATFORM_BROWSER_ID} from '@angular/common/src/platform_id';
import {afterNextRender, ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, Directive, inject, Input, PLATFORM_ID, signal, ViewChild} from '@angular/core';
import {TestBed} from '@angular/core/testing';

describe('CheckAlways components', () => {
Expand Down Expand Up @@ -823,6 +824,31 @@ describe('OnPush components with signals', () => {
fixture.componentInstance.cdr.detectChanges();
expect(fixture.nativeElement.innerText).toEqual('2');
});

// Note: We probably don't want this to throw but need to decide how to handle reruns
// This asserts current behavior and should be updated when this is handled
it('throws error when writing to a signal in afterRender', () => {
const counter = signal(0);

@Component({
selector: 'test-component',
standalone: true,
template: ` {{counter()}} `,
})
class TestCmp {
counter = counter;
constructor() {
afterNextRender(() => {
this.counter.set(1);
});
}
}
TestBed.configureTestingModule(
{providers: [{provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID}]});

const fixture = TestBed.createComponent(TestCmp);
expect(() => fixture.detectChanges()).toThrowError(/ExpressionChanged/);
});
});


Expand Down