Skip to content

Commit 702ec90

Browse files
atscottAndrewKushnir
authored andcommitted
fix(core): When using setInput, mark view dirty in same way as markForCheck (#49747)
`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 #49747
1 parent ffdfdc2 commit 702ec90

File tree

9 files changed

+57
-14
lines changed

9 files changed

+57
-14
lines changed

packages/core/src/render3/component_ref.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {getNodeInjectable, NodeInjector} from './di';
3131
import {throwProviderNotFoundError} from './errors_di';
3232
import {registerPostOrderHooks} from './hooks';
3333
import {reportUnknownPropertyError} from './instructions/element_validation';
34-
import {addToViewTree, createLView, createTView, executeContentQueries, getOrCreateComponentTView, getOrCreateTNode, initializeDirectives, invokeDirectivesHostBindings, locateHostElement, markAsComponentHost, markDirtyIfOnPush, renderView, setInputsForProperty} from './instructions/shared';
34+
import {addToViewTree, createLView, createTView, executeContentQueries, getOrCreateComponentTView, getOrCreateTNode, initializeDirectives, invokeDirectivesHostBindings, locateHostElement, markAsComponentHost, markViewDirty, renderView, setInputsForProperty} from './instructions/shared';
3535
import {ComponentDef, DirectiveDef, HostDirectiveDefs} from './interfaces/definition';
3636
import {PropertyAliasValue, TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeType} from './interfaces/node';
3737
import {Renderer, RendererFactory} from './interfaces/renderer';
@@ -44,7 +44,7 @@ import {enterView, getCurrentTNode, getLView, leaveView} from './state';
4444
import {computeStaticStyling} from './styling/static_styling';
4545
import {mergeHostAttrs, setUpAttributes} from './util/attrs_utils';
4646
import {stringifyForError} from './util/stringify_utils';
47-
import {getNativeByTNode, getTNode} from './util/view_utils';
47+
import {getComponentLViewByIndex, getNativeByTNode, getTNode} from './util/view_utils';
4848
import {RootViewRef, ViewRef} from './view_ref';
4949

5050
export class ComponentFactoryResolver extends AbstractComponentFactoryResolver {
@@ -268,7 +268,8 @@ export class ComponentRef<T> extends AbstractComponentRef<T> {
268268
if (inputData !== null && (dataValue = inputData[name])) {
269269
const lView = this._rootLView;
270270
setInputsForProperty(lView[TVIEW], lView, dataValue, name, value);
271-
markDirtyIfOnPush(lView, this._tNode.index);
271+
const childComponentLView = getComponentLViewByIndex(this._tNode.index, lView);
272+
markViewDirty(childComponentLView);
272273
} else {
273274
if (ngDevMode) {
274275
const cmpNameForError = stringifyForError(this.componentType);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1200,7 +1200,7 @@
12001200
"name": "markAsComponentHost"
12011201
},
12021202
{
1203-
"name": "markDirtyIfOnPush"
1203+
"name": "markViewDirty"
12041204
},
12051205
{
12061206
"name": "maybeWrapInNotSelector"

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,9 @@
914914
{
915915
"name": "markAsComponentHost"
916916
},
917+
{
918+
"name": "markViewDirty"
919+
},
917920
{
918921
"name": "maybeWrapInNotSelector"
919922
},

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,9 +1307,6 @@
13071307
{
13081308
"name": "markAsComponentHost"
13091309
},
1310-
{
1311-
"name": "markDirtyIfOnPush"
1312-
},
13131310
{
13141311
"name": "markDuplicates"
13151312
},

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
@@ -1265,9 +1265,6 @@
12651265
{
12661266
"name": "markAsComponentHost"
12671267
},
1268-
{
1269-
"name": "markDirtyIfOnPush"
1270-
},
12711268
{
12721269
"name": "markDuplicates"
12731270
},

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,9 @@
698698
{
699699
"name": "makeRecord"
700700
},
701+
{
702+
"name": "markViewDirty"
703+
},
701704
{
702705
"name": "maybeWrapInNotSelector"
703706
},

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,9 @@
800800
{
801801
"name": "makeRecord"
802802
},
803+
{
804+
"name": "markViewDirty"
805+
},
803806
{
804807
"name": "maybeWrapInNotSelector"
805808
},

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,9 +1112,6 @@
11121112
{
11131113
"name": "markAsComponentHost"
11141114
},
1115-
{
1116-
"name": "markDirtyIfOnPush"
1117-
},
11181115
{
11191116
"name": "markDuplicates"
11201117
},

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';
@@ -397,5 +398,46 @@ describe('ComponentFactory', () => {
397398
fixture.detectChanges();
398399
expect(fixture.nativeElement.textContent).toBe('pushed');
399400
});
401+
402+
it('marks parents dirty so component is not "shielded" by a non-dirty OnPush parent', () => {
403+
@Component({
404+
template: `{{input}}`,
405+
standalone: true,
406+
selector: 'dynamic',
407+
})
408+
class DynamicCmp {
409+
@Input() input?: string;
410+
}
411+
412+
@Component({
413+
template: '<ng-template #template></ng-template>',
414+
standalone: true,
415+
imports: [DynamicCmp],
416+
changeDetection: ChangeDetectionStrategy.OnPush,
417+
})
418+
class Wrapper {
419+
@ViewChild('template', {read: ViewContainerRef}) template?: ViewContainerRef;
420+
componentRef?: ComponentRef<DynamicCmp>;
421+
422+
create() {
423+
this.componentRef = this.template!.createComponent(DynamicCmp);
424+
}
425+
setInput(value: string) {
426+
this.componentRef!.setInput('input', value);
427+
}
428+
}
429+
430+
const fixture = TestBed.createComponent(Wrapper);
431+
fixture.detectChanges();
432+
fixture.componentInstance.create();
433+
434+
fixture.componentInstance.setInput('1');
435+
fixture.detectChanges();
436+
expect(fixture.nativeElement.textContent).toBe('1');
437+
438+
fixture.componentInstance.setInput('2');
439+
fixture.detectChanges();
440+
expect(fixture.nativeElement.textContent).toBe('2');
441+
});
400442
});
401443
});

0 commit comments

Comments
 (0)