Skip to content

Commit 07920d9

Browse files
atscottAndrewKushnir
authored andcommitted
fix(core): Reattached views that are dirty from a signal update should refresh (#53001)
Related to #52928 but `updateAncestorTraversalFlagsOnAttach` is called on view insertion and _should_ have made that work for views dirty from signals but it wasn't updated to read the `dirty` flag when we changed it from sharing the `RefreshView` flag. For #52928, we've traditionally worked under the assumption that this is working as expected. The created view is `CheckAlways`. There is a question of whether we should automatically mark things for check when the attached view has the `Dirty` flag and/or has the `FirstLViewPass` flag set (or other flags that indicate it definitely needs to be prefreshed). PR Close #53001
1 parent 42272a0 commit 07920d9

File tree

14 files changed

+62
-15
lines changed

14 files changed

+62
-15
lines changed

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {CONTEXT, EFFECTS_TO_SCHEDULE, ENVIRONMENT, FLAGS, InitPhaseState, LView,
1818
import {getOrBorrowReactiveLViewConsumer, maybeReturnReactiveLViewConsumer, ReactiveLViewConsumer} from '../reactive_lview_consumer';
1919
import {enterView, isInCheckNoChangesMode, leaveView, setBindingIndex, setIsInCheckNoChangesMode} from '../state';
2020
import {getFirstLContainer, getNextLContainer} from '../util/view_traversal_utils';
21-
import {getComponentLViewByIndex, isCreationMode, markAncestorsForTraversal, markViewForRefresh, resetPreOrderHookFlags, viewAttachedToChangeDetector} from '../util/view_utils';
21+
import {getComponentLViewByIndex, isCreationMode, markAncestorsForTraversal, markViewForRefresh, requiresRefreshOrTraversal, resetPreOrderHookFlags, viewAttachedToChangeDetector} from '../util/view_utils';
2222

2323
import {executeTemplate, executeViewQueryFn, handleError, processHostBindingOpCodes, refreshContentQueries} from './shared';
2424

@@ -72,8 +72,7 @@ function detectChangesInViewWhileDirty(lView: LView) {
7272
// descendants views that need to be refreshed due to re-dirtying during the change detection
7373
// run, detect changes on the view again. We run change detection in `Targeted` mode to only
7474
// refresh views with the `RefreshView` flag.
75-
while (lView[FLAGS] & (LViewFlags.RefreshView | LViewFlags.HasChildViewsToRefresh) ||
76-
lView[REACTIVE_TEMPLATE_CONSUMER]?.dirty) {
75+
while (requiresRefreshOrTraversal(lView)) {
7776
if (retries === MAXIMUM_REFRESH_RERUNS) {
7877
throw new RuntimeError(
7978
RuntimeErrorCode.INFINITE_CHANGE_DETECTION,

packages/core/src/render3/util/view_utils.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {LContainer, LContainerFlags, TYPE} from '../interfaces/container';
1313
import {TConstants, TNode} from '../interfaces/node';
1414
import {RNode} from '../interfaces/renderer_dom';
1515
import {isLContainer, isLView} from '../interfaces/type_checks';
16-
import {DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, ON_DESTROY_HOOKS, PARENT, PREORDER_HOOK_FLAGS, PreOrderHookFlags, TData, TView} from '../interfaces/view';
16+
import {DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, ON_DESTROY_HOOKS, PARENT, PREORDER_HOOK_FLAGS, PreOrderHookFlags, REACTIVE_TEMPLATE_CONSUMER, TData, TView} from '../interfaces/view';
1717

1818

1919

@@ -195,21 +195,22 @@ export function walkUpViews(nestingLevel: number, currentView: LView): LView {
195195
return currentView;
196196
}
197197

198+
export function requiresRefreshOrTraversal(lView: LView) {
199+
return lView[FLAGS] & (LViewFlags.RefreshView | LViewFlags.HasChildViewsToRefresh) ||
200+
lView[REACTIVE_TEMPLATE_CONSUMER]?.dirty;
201+
}
202+
198203

199204
/**
200-
* Updates the `DESCENDANT_VIEWS_TO_REFRESH` counter on the parents of the `LView` as well as the
201-
* parents above that whose
202-
* 1. counter goes from 0 to 1, indicating that there is a new child that has a view to refresh
203-
* or
204-
* 2. counter goes from 1 to 0, indicating there are no more descendant views to refresh
205-
* When attaching/re-attaching an `LView` to the change detection tree, we need to ensure that the
206-
* views above it are traversed during change detection if this one is marked for refresh or has
207-
* some child or descendant that needs to be refreshed.
205+
* Updates the `HasChildViewsToRefresh` flag on the parents of the `LView` as well as the
206+
* parents above.
208207
*/
209208
export function updateAncestorTraversalFlagsOnAttach(lView: LView) {
210-
if (lView[FLAGS] & (LViewFlags.RefreshView | LViewFlags.HasChildViewsToRefresh)) {
211-
markAncestorsForTraversal(lView);
209+
if (!requiresRefreshOrTraversal(lView)) {
210+
return;
212211
}
212+
213+
markAncestorsForTraversal(lView);
213214
}
214215

215216
/**

packages/core/test/acceptance/change_detection_signals_in_zones_spec.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -793,7 +793,9 @@ describe('OnPush components with signals', () => {
793793

794794
@Component({
795795
selector: 'on-push-parent',
796-
template: `<signal-component></signal-component>{{incrementChecks()}}`,
796+
template: `
797+
<signal-component></signal-component>
798+
{{incrementChecks()}}`,
797799
changeDetection: ChangeDetectionStrategy.OnPush,
798800
standalone: true,
799801
imports: [SignalComponent],
@@ -827,6 +829,18 @@ describe('OnPush components with signals', () => {
827829
expect(trim(fixture.nativeElement.textContent)).toEqual('initial');
828830
});
829831

832+
it('refreshes when reattached if already dirty', () => {
833+
const fixture = TestBed.createComponent(OnPushParent);
834+
fixture.detectChanges();
835+
fixture.componentInstance.signalChild.value.set('new');
836+
fixture.componentInstance.signalChild.cdr.detach();
837+
fixture.detectChanges();
838+
expect(trim(fixture.nativeElement.textContent)).toEqual('initial');
839+
fixture.componentInstance.signalChild.cdr.reattach();
840+
fixture.detectChanges();
841+
expect(trim(fixture.nativeElement.textContent)).toEqual('new');
842+
});
843+
830844
// Note: Design decision for signals because that's how the hooks work today
831845
// We have considered actually running a component's `afterViewChecked` hook if it's refreshed
832846
// in targeted mode (meaning the parent did not refresh) and could change this decision.

packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1328,6 +1328,9 @@
13281328
{
13291329
"name": "replacePostStylesAsPre"
13301330
},
1331+
{
1332+
"name": "requiresRefreshOrTraversal"
1333+
},
13311334
{
13321335
"name": "resetPreOrderHookFlags"
13331336
},

packages/core/test/bundling/animations/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1400,6 +1400,9 @@
14001400
{
14011401
"name": "replacePostStylesAsPre"
14021402
},
1403+
{
1404+
"name": "requiresRefreshOrTraversal"
1405+
},
14031406
{
14041407
"name": "resetPreOrderHookFlags"
14051408
},

packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,6 +1112,9 @@
11121112
{
11131113
"name": "renderView"
11141114
},
1115+
{
1116+
"name": "requiresRefreshOrTraversal"
1117+
},
11151118
{
11161119
"name": "resetPreOrderHookFlags"
11171120
},

packages/core/test/bundling/defer/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2249,6 +2249,9 @@
22492249
{
22502250
"name": "renderView"
22512251
},
2252+
{
2253+
"name": "requiresRefreshOrTraversal"
2254+
},
22522255
{
22532256
"name": "resetPreOrderHookFlags"
22542257
},

packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1553,6 +1553,9 @@
15531553
{
15541554
"name": "renderView"
15551555
},
1556+
{
1557+
"name": "requiresRefreshOrTraversal"
1558+
},
15561559
{
15571560
"name": "resetPreOrderHookFlags"
15581561
},

packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1523,6 +1523,9 @@
15231523
{
15241524
"name": "requiredValidator"
15251525
},
1526+
{
1527+
"name": "requiresRefreshOrTraversal"
1528+
},
15261529
{
15271530
"name": "resetPreOrderHookFlags"
15281531
},

packages/core/test/bundling/hello_world/bundle.golden_symbols.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,9 @@
881881
{
882882
"name": "renderView"
883883
},
884+
{
885+
"name": "requiresRefreshOrTraversal"
886+
},
884887
{
885888
"name": "resetPreOrderHookFlags"
886889
},

0 commit comments

Comments
 (0)