Skip to content

Commit 96c6139

Browse files
pkozlowski-opensourceatscott
authored andcommitted
feat(core): add ability to set inputs on ComponentRef (#46641)
This change adds the setInput method to the ComponentRef. Previously users had to call `componentRef.instance['inputName']` to change inputs of a dynamically created component. This had several problems: * OnPush components were not marked for check and thus very difficult to test; * input aliasing was not take into account - a property name on a component could have been different from the actual input name so setting input properties was fragile; * manually setting input propertie would NOT trigger the `NgOnChanges` lifecycle hook. This modifications unifies `@Input` accross dynamically created components and the ones referenced in templates. This also opens doors to other changes: as an example router could use this new method to set `@Input`s from router params. Closes #12313 Closes #22567 PR Close #46641
1 parent dd3e096 commit 96c6139

15 files changed

Lines changed: 192 additions & 10 deletions

File tree

goldens/public-api/core/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ export abstract class ComponentRef<C> {
235235
abstract get instance(): C;
236236
abstract get location(): ElementRef;
237237
abstract onDestroy(callback: Function): void;
238+
abstract setInput(name: string, value: unknown): void;
238239
}
239240

240241
// @public

goldens/size-tracking/integration-payloads.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"cli-hello-world-lazy": {
3434
"uncompressed": {
3535
"runtime": 2835,
36-
"main": 236657,
36+
"main": 237313,
3737
"polyfills": 33842,
3838
"src_app_lazy_lazy_module_ts": 780
3939
}
@@ -68,4 +68,4 @@
6868
"bundle": 1214857
6969
}
7070
}
71-
}
71+
}

packages/core/src/linker/component_factory.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ import {ViewRef} from './view_ref';
2323
* @publicApi
2424
*/
2525
export abstract class ComponentRef<C> {
26+
/**
27+
* Updates a specified input name to a new value. Using this method will properly mark for check
28+
* component using the `OnPush` change detection strategy. It will also assure that the
29+
* `OnChanges` lifecycle hook runs when a dynamically created component is change-detected.
30+
*
31+
* @param name The name of an input.
32+
* @param value The new value of an input.
33+
*/
34+
abstract setInput(name: string, value: unknown): void;
35+
2636
/**
2737
* The host or anchor [element](guide/glossary#element) for this component instance.
2838
*/

packages/core/src/render3/component_ref.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {Injector} from '../di/injector';
1111
import {InjectFlags} from '../di/interface/injector';
1212
import {ProviderToken} from '../di/provider_token';
1313
import {EnvironmentInjector} from '../di/r3_injector';
14-
import {RuntimeError, RuntimeErrorCode} from '../errors';
14+
import {formatRuntimeError, RuntimeError, RuntimeErrorCode} from '../errors';
1515
import {Type} from '../interface/type';
1616
import {ComponentFactory as viewEngine_ComponentFactory, ComponentRef as viewEngine_ComponentRef} from '../linker/component_factory';
1717
import {ComponentFactoryResolver as viewEngine_ComponentFactoryResolver} from '../linker/component_factory_resolver';
@@ -26,16 +26,18 @@ import {assertComponentType} from './assert';
2626
import {createRootComponent, createRootComponentView, createRootContext, LifecycleHooksFeature} from './component';
2727
import {getComponentDef} from './definition';
2828
import {NodeInjector} from './di';
29-
import {createLView, createTView, locateHostElement, renderView} from './instructions/shared';
29+
import {reportUnknownPropertyError} from './instructions/element_validation';
30+
import {createLView, createTView, initializeInputAndOutputAliases, locateHostElement, markDirtyIfOnPush, renderView, setInputsForProperty} from './instructions/shared';
3031
import {ComponentDef} from './interfaces/definition';
31-
import {TContainerNode, TElementContainerNode, TElementNode, TNode} from './interfaces/node';
32+
import {PropertyAliasValue, TContainerNode, TElementContainerNode, TElementNode, TNode} from './interfaces/node';
3233
import {RNode} from './interfaces/renderer_dom';
33-
import {HEADER_OFFSET, LView, LViewFlags, TViewType} from './interfaces/view';
34+
import {HEADER_OFFSET, LView, LViewFlags, TVIEW, TViewType} from './interfaces/view';
3435
import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from './namespaces';
3536
import {createElementNode, writeDirectClass} from './node_manipulation';
3637
import {extractAttrsAndClassesFromSelector, stringifyCSSSelectorList} from './node_selector_matcher';
3738
import {enterView, leaveView} from './state';
3839
import {setUpAttributes} from './util/attrs_utils';
40+
import {stringifyForError} from './util/stringify_utils';
3941
import {getTNode} from './util/view_utils';
4042
import {RootViewRef, ViewRef} from './view_ref';
4143

@@ -226,7 +228,6 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
226228
// Angular 5 reference: https://stackblitz.com/edit/lifecycle-hooks-vcref
227229
component = createRootComponent(
228230
componentView, this.componentDef, rootLView, rootContext, [LifecycleHooksFeature]);
229-
230231
renderView(rootTView, rootLView, null);
231232
} finally {
232233
leaveView();
@@ -275,6 +276,25 @@ export class ComponentRef<T> extends viewEngine_ComponentRef<T> {
275276
this.componentType = componentType;
276277
}
277278

279+
override setInput(name: string, value: unknown): void {
280+
const inputData = this._tNode.inputs;
281+
let dataValue: PropertyAliasValue|undefined;
282+
if (inputData !== null && (dataValue = inputData[name])) {
283+
const lView = this._rootLView;
284+
setInputsForProperty(lView[TVIEW], lView, dataValue, name, value);
285+
markDirtyIfOnPush(lView, this._tNode.index);
286+
} else {
287+
if (ngDevMode) {
288+
const cmpNameForError = stringifyForError(this.componentType);
289+
let message =
290+
`Can't set value of the '${name}' input on the '${cmpNameForError}' component. `;
291+
message += `Make sure that the '${
292+
name}' property is annotated with @Input() or a mapped @Input('${name}') exists.`;
293+
reportUnknownPropertyError(message);
294+
}
295+
}
296+
}
297+
278298
override get injector(): Injector {
279299
return new NodeInjector(this._tNode, this._rootLView);
280300
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,10 @@ export function handleUnknownPropertyError(
207207
}
208208
}
209209

210+
reportUnknownPropertyError(message);
211+
}
212+
213+
export function reportUnknownPropertyError(message: string) {
210214
if (shouldThrowErrorOnUnknownProperty) {
211215
throw new RuntimeError(RuntimeErrorCode.UNKNOWN_BINDING, message);
212216
} else {

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -912,7 +912,7 @@ function generatePropertyAliases(
912912
* Initializes data structures required to work with directive inputs and outputs.
913913
* Initialization is done for all directives matched on a given TNode.
914914
*/
915-
function initializeInputAndOutputAliases(tView: TView, tNode: TNode): void {
915+
export function initializeInputAndOutputAliases(tView: TView, tNode: TNode): void {
916916
ngDevMode && assertFirstCreatePass(tView);
917917

918918
const start = tNode.directiveStart;
@@ -1010,7 +1010,7 @@ export function elementPropertyInternal<T>(
10101010
}
10111011

10121012
/** If node is an OnPush component, marks its LView dirty. */
1013-
function markDirtyIfOnPush(lView: LView, viewIndex: number): void {
1013+
export function markDirtyIfOnPush(lView: LView, viewIndex: number): void {
10141014
ngDevMode && assertLView(lView);
10151015
const childComponentLView = getComponentLViewByIndex(viewIndex, lView);
10161016
if (!(childComponentLView[FLAGS] & LViewFlags.CheckAlways)) {
@@ -1067,6 +1067,7 @@ export function instantiateRootComponent<T>(tView: TView, lView: LView, def: Com
10671067
directiveIndex, rootTNode.directiveStart,
10681068
'Because this is a root component the allocated expando should match the TNode component.');
10691069
configureViewWithDirective(tView, rootTNode, lView, directiveIndex, def);
1070+
initializeInputAndOutputAliases(tView, rootTNode);
10701071
}
10711072
const directive =
10721073
getNodeInjectable(lView, tView, rootTNode.directiveStart, rootTNode as TElementNode);

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -932,6 +932,9 @@
932932
{
933933
"name": "initTNodeFlags"
934934
},
935+
{
936+
"name": "initializeInputAndOutputAliases"
937+
},
935938
{
936939
"name": "injectArgs"
937940
},
@@ -1085,6 +1088,9 @@
10851088
{
10861089
"name": "markAsComponentHost"
10871090
},
1091+
{
1092+
"name": "markDirtyIfOnPush"
1093+
},
10881094
{
10891095
"name": "maybeWrapInNotSelector"
10901096
},

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,9 @@
698698
{
699699
"name": "initTNodeFlags"
700700
},
701+
{
702+
"name": "initializeInputAndOutputAliases"
703+
},
701704
{
702705
"name": "injectArgs"
703706
},
@@ -965,6 +968,9 @@
965968
{
966969
"name": "setInjectImplementation"
967970
},
971+
{
972+
"name": "setInputsForProperty"
973+
},
968974
{
969975
"name": "setInputsFromAttrs"
970976
},

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,6 +1028,9 @@
10281028
{
10291029
"name": "initTNodeFlags"
10301030
},
1031+
{
1032+
"name": "initializeInputAndOutputAliases"
1033+
},
10311034
{
10321035
"name": "injectArgs"
10331036
},
@@ -1199,6 +1202,9 @@
11991202
{
12001203
"name": "markAsComponentHost"
12011204
},
1205+
{
1206+
"name": "markDirtyIfOnPush"
1207+
},
12021208
{
12031209
"name": "markDuplicates"
12041210
},

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -992,6 +992,9 @@
992992
{
993993
"name": "initTNodeFlags"
994994
},
995+
{
996+
"name": "initializeInputAndOutputAliases"
997+
},
995998
{
996999
"name": "injectArgs"
9971000
},
@@ -1160,6 +1163,9 @@
11601163
{
11611164
"name": "markAsComponentHost"
11621165
},
1166+
{
1167+
"name": "markDirtyIfOnPush"
1168+
},
11631169
{
11641170
"name": "markDuplicates"
11651171
},

0 commit comments

Comments
 (0)