Skip to content

Commit a4e749f

Browse files
atscottdylhunn
authored andcommitted
fix(core): When using setInput, mark view dirty in same was as markForCheck (#49711)
`ComponentRef.setInput` internally calls `markDirtyIfOnPush` which only marks the given view as dirty but does not mark parents dirty like `ChangeDetectorRef.markForCheck` would. https://github.com/angular/angular/blob/f071224720f8affb97fd32fb5aeaa13155b13693/packages/core/src/render3/instructions/shared.ts#L1018-L1024 `markDirtyIfOnPush` has an assumption that it’s being called from the parent’s template. That is, we don’t need to mark dirty to the root, because we’ve already traversed down to it. The function used to only be called during template execution for input bindings but was added to `setInput` later. It's not a good fit because it means that if you are responding to events such as an emit from an `Observable` and call `setInput`, the view of your `ComponentRef` won't necessarily get checked when change detection runs next. If this lives inside some `OnPush` component tree that's not already dirty, it only gets refreshed if you also call `ChangeDetectorRef.markForCheck` in the host component (because it will be "shielded" be a non-dirty parent). PR Close #49711
1 parent aad05eb commit a4e749f

File tree

6 files changed

+49
-17
lines changed

6 files changed

+49
-17
lines changed

packages/core/src/render3/component_ref.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,11 @@ import {getNodeInjectable, NodeInjector} from './di';
3333
import {throwProviderNotFoundError} from './errors_di';
3434
import {registerPostOrderHooks} from './hooks';
3535
import {reportUnknownPropertyError} from './instructions/element_validation';
36-
import {addToViewTree, createLView, createTView, executeContentQueries, getOrCreateComponentTView, getOrCreateTNode, initializeDirectives, invokeDirectivesHostBindings, locateHostElement, markAsComponentHost, markDirtyIfOnPush, renderView, setInputsForProperty} from './instructions/shared';
36+
import {markViewDirty} from './instructions/mark_view_dirty';
37+
import {addToViewTree, createLView, createTView, executeContentQueries, getOrCreateComponentTView, getOrCreateTNode, initializeDirectives, invokeDirectivesHostBindings, locateHostElement, markAsComponentHost, renderView, setInputsForProperty} from './instructions/shared';
3738
import {ComponentDef, DirectiveDef, HostDirectiveDefs} from './interfaces/definition';
3839
import {PropertyAliasValue, TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeType} from './interfaces/node';
39-
import {Renderer, RendererFactory} from './interfaces/renderer';
40+
import {Renderer} from './interfaces/renderer';
4041
import {RElement, RNode} from './interfaces/renderer_dom';
4142
import {CONTEXT, HEADER_OFFSET, INJECTOR, LView, LViewEnvironment, LViewFlags, TVIEW, TViewType} from './interfaces/view';
4243
import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from './namespaces';
@@ -47,7 +48,7 @@ import {enterView, getCurrentTNode, getLView, leaveView} from './state';
4748
import {computeStaticStyling} from './styling/static_styling';
4849
import {mergeHostAttrs, setUpAttributes} from './util/attrs_utils';
4950
import {stringifyForError} from './util/stringify_utils';
50-
import {getNativeByTNode, getTNode} from './util/view_utils';
51+
import {getComponentLViewByIndex, getNativeByTNode, getTNode} from './util/view_utils';
5152
import {RootViewRef, ViewRef} from './view_ref';
5253

5354
export class ComponentFactoryResolver extends AbstractComponentFactoryResolver {
@@ -291,7 +292,8 @@ export class ComponentRef<T> extends AbstractComponentRef<T> {
291292
const lView = this._rootLView;
292293
setInputsForProperty(lView[TVIEW], lView, dataValue, name, value);
293294
this.previousInputValues.set(name, value);
294-
markDirtyIfOnPush(lView, this._tNode.index);
295+
const childComponentLView = getComponentLViewByIndex(this._tNode.index, lView);
296+
markViewDirty(childComponentLView);
295297
} else {
296298
if (ngDevMode) {
297299
const cmpNameForError = stringifyForError(this.componentType);

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,9 +1265,6 @@
12651265
{
12661266
"name": "markAsComponentHost"
12671267
},
1268-
{
1269-
"name": "markDirtyIfOnPush"
1270-
},
12711268
{
12721269
"name": "markViewDirty"
12731270
},

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1376,9 +1376,6 @@
13761376
{
13771377
"name": "markAsComponentHost"
13781378
},
1379-
{
1380-
"name": "markDirtyIfOnPush"
1381-
},
13821379
{
13831380
"name": "markDuplicates"
13841381
},

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1334,9 +1334,6 @@
13341334
{
13351335
"name": "markAsComponentHost"
13361336
},
1337-
{
1338-
"name": "markDirtyIfOnPush"
1339-
},
13401337
{
13411338
"name": "markDuplicates"
13421339
},

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,9 +1193,6 @@
11931193
{
11941194
"name": "markAsComponentHost"
11951195
},
1196-
{
1197-
"name": "markDirtyIfOnPush"
1198-
},
11991196
{
12001197
"name": "markDuplicates"
12011198
},

packages/core/test/render3/component_ref_spec.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {ComponentRef} from '@angular/core';
910
import {ComponentFactoryResolver} from '@angular/core/src/render3/component_ref';
1011
import {Renderer} from '@angular/core/src/render3/interfaces/renderer';
1112
import {RElement} from '@angular/core/src/render3/interfaces/renderer_dom';
1213
import {TestBed} from '@angular/core/testing';
1314

14-
import {ChangeDetectionStrategy, Component, Injector, Input, NgModuleRef, OnChanges, Output, RendererType2, SimpleChanges, ViewEncapsulation} from '../../src/core';
15+
import {ChangeDetectionStrategy, Component, Injector, Input, NgModuleRef, OnChanges, Output, RendererType2, SimpleChanges, ViewChild, ViewContainerRef, ViewEncapsulation} from '../../src/core';
1516
import {ComponentFactory} from '../../src/linker/component_factory';
1617
import {RendererFactory2} from '../../src/render/api';
1718
import {Sanitizer} from '../../src/sanitization/sanitizer';
@@ -420,5 +421,46 @@ describe('ComponentFactory', () => {
420421
fixture.detectChanges();
421422
expect(log).toEqual(['1', '2']);
422423
});
424+
425+
it('marks parents dirty so component is not "shielded" by a non-dirty OnPush parent', () => {
426+
@Component({
427+
template: `{{input}}`,
428+
standalone: true,
429+
selector: 'dynamic',
430+
})
431+
class DynamicCmp {
432+
@Input() input?: string;
433+
}
434+
435+
@Component({
436+
template: '<ng-template #template></ng-template>',
437+
standalone: true,
438+
imports: [DynamicCmp],
439+
changeDetection: ChangeDetectionStrategy.OnPush,
440+
})
441+
class Wrapper {
442+
@ViewChild('template', {read: ViewContainerRef}) template?: ViewContainerRef;
443+
componentRef?: ComponentRef<DynamicCmp>;
444+
445+
create() {
446+
this.componentRef = this.template!.createComponent(DynamicCmp);
447+
}
448+
setInput(value: string) {
449+
this.componentRef!.setInput('input', value);
450+
}
451+
}
452+
453+
const fixture = TestBed.createComponent(Wrapper);
454+
fixture.detectChanges();
455+
fixture.componentInstance.create();
456+
457+
fixture.componentInstance.setInput('1');
458+
fixture.detectChanges();
459+
expect(fixture.nativeElement.innerText).toBe('1');
460+
461+
fixture.componentInstance.setInput('2');
462+
fixture.detectChanges();
463+
expect(fixture.nativeElement.innerText).toBe('2');
464+
});
423465
});
424466
});

0 commit comments

Comments
 (0)