Skip to content

Commit

Permalink
feat(core): add ability to set inputs on ComponentRef (#46641)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
pkozlowski-opensource authored and atscott committed Jul 1, 2022
1 parent dd3e096 commit 96c6139
Show file tree
Hide file tree
Showing 15 changed files with 192 additions and 10 deletions.
1 change: 1 addition & 0 deletions goldens/public-api/core/index.md
Expand Up @@ -235,6 +235,7 @@ export abstract class ComponentRef<C> {
abstract get instance(): C;
abstract get location(): ElementRef;
abstract onDestroy(callback: Function): void;
abstract setInput(name: string, value: unknown): void;
}

// @public
Expand Down
4 changes: 2 additions & 2 deletions goldens/size-tracking/integration-payloads.json
Expand Up @@ -33,7 +33,7 @@
"cli-hello-world-lazy": {
"uncompressed": {
"runtime": 2835,
"main": 236657,
"main": 237313,
"polyfills": 33842,
"src_app_lazy_lazy_module_ts": 780
}
Expand Down Expand Up @@ -68,4 +68,4 @@
"bundle": 1214857
}
}
}
}
10 changes: 10 additions & 0 deletions packages/core/src/linker/component_factory.ts
Expand Up @@ -23,6 +23,16 @@ import {ViewRef} from './view_ref';
* @publicApi
*/
export abstract class ComponentRef<C> {
/**
* Updates a specified input name to a new value. Using this method will properly mark for check
* component using the `OnPush` change detection strategy. It will also assure that the
* `OnChanges` lifecycle hook runs when a dynamically created component is change-detected.
*
* @param name The name of an input.
* @param value The new value of an input.
*/
abstract setInput(name: string, value: unknown): void;

/**
* The host or anchor [element](guide/glossary#element) for this component instance.
*/
Expand Down
30 changes: 25 additions & 5 deletions packages/core/src/render3/component_ref.ts
Expand Up @@ -11,7 +11,7 @@ import {Injector} from '../di/injector';
import {InjectFlags} from '../di/interface/injector';
import {ProviderToken} from '../di/provider_token';
import {EnvironmentInjector} from '../di/r3_injector';
import {RuntimeError, RuntimeErrorCode} from '../errors';
import {formatRuntimeError, RuntimeError, RuntimeErrorCode} from '../errors';
import {Type} from '../interface/type';
import {ComponentFactory as viewEngine_ComponentFactory, ComponentRef as viewEngine_ComponentRef} from '../linker/component_factory';
import {ComponentFactoryResolver as viewEngine_ComponentFactoryResolver} from '../linker/component_factory_resolver';
Expand All @@ -26,16 +26,18 @@ import {assertComponentType} from './assert';
import {createRootComponent, createRootComponentView, createRootContext, LifecycleHooksFeature} from './component';
import {getComponentDef} from './definition';
import {NodeInjector} from './di';
import {createLView, createTView, locateHostElement, renderView} from './instructions/shared';
import {reportUnknownPropertyError} from './instructions/element_validation';
import {createLView, createTView, initializeInputAndOutputAliases, locateHostElement, markDirtyIfOnPush, renderView, setInputsForProperty} from './instructions/shared';
import {ComponentDef} from './interfaces/definition';
import {TContainerNode, TElementContainerNode, TElementNode, TNode} from './interfaces/node';
import {PropertyAliasValue, TContainerNode, TElementContainerNode, TElementNode, TNode} from './interfaces/node';
import {RNode} from './interfaces/renderer_dom';
import {HEADER_OFFSET, LView, LViewFlags, TViewType} from './interfaces/view';
import {HEADER_OFFSET, LView, LViewFlags, TVIEW, TViewType} from './interfaces/view';
import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from './namespaces';
import {createElementNode, writeDirectClass} from './node_manipulation';
import {extractAttrsAndClassesFromSelector, stringifyCSSSelectorList} from './node_selector_matcher';
import {enterView, leaveView} from './state';
import {setUpAttributes} from './util/attrs_utils';
import {stringifyForError} from './util/stringify_utils';
import {getTNode} from './util/view_utils';
import {RootViewRef, ViewRef} from './view_ref';

Expand Down Expand Up @@ -226,7 +228,6 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
// Angular 5 reference: https://stackblitz.com/edit/lifecycle-hooks-vcref
component = createRootComponent(
componentView, this.componentDef, rootLView, rootContext, [LifecycleHooksFeature]);

renderView(rootTView, rootLView, null);
} finally {
leaveView();
Expand Down Expand Up @@ -275,6 +276,25 @@ export class ComponentRef<T> extends viewEngine_ComponentRef<T> {
this.componentType = componentType;
}

override setInput(name: string, value: unknown): void {
const inputData = this._tNode.inputs;
let dataValue: PropertyAliasValue|undefined;
if (inputData !== null && (dataValue = inputData[name])) {
const lView = this._rootLView;
setInputsForProperty(lView[TVIEW], lView, dataValue, name, value);
markDirtyIfOnPush(lView, this._tNode.index);
} else {
if (ngDevMode) {
const cmpNameForError = stringifyForError(this.componentType);
let message =
`Can't set value of the '${name}' input on the '${cmpNameForError}' component. `;
message += `Make sure that the '${
name}' property is annotated with @Input() or a mapped @Input('${name}') exists.`;
reportUnknownPropertyError(message);
}
}
}

override get injector(): Injector {
return new NodeInjector(this._tNode, this._rootLView);
}
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/render3/instructions/element_validation.ts
Expand Up @@ -207,6 +207,10 @@ export function handleUnknownPropertyError(
}
}

reportUnknownPropertyError(message);
}

export function reportUnknownPropertyError(message: string) {
if (shouldThrowErrorOnUnknownProperty) {
throw new RuntimeError(RuntimeErrorCode.UNKNOWN_BINDING, message);
} else {
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/render3/instructions/shared.ts
Expand Up @@ -912,7 +912,7 @@ function generatePropertyAliases(
* Initializes data structures required to work with directive inputs and outputs.
* Initialization is done for all directives matched on a given TNode.
*/
function initializeInputAndOutputAliases(tView: TView, tNode: TNode): void {
export function initializeInputAndOutputAliases(tView: TView, tNode: TNode): void {
ngDevMode && assertFirstCreatePass(tView);

const start = tNode.directiveStart;
Expand Down Expand Up @@ -1010,7 +1010,7 @@ export function elementPropertyInternal<T>(
}

/** If node is an OnPush component, marks its LView dirty. */
function markDirtyIfOnPush(lView: LView, viewIndex: number): void {
export function markDirtyIfOnPush(lView: LView, viewIndex: number): void {
ngDevMode && assertLView(lView);
const childComponentLView = getComponentLViewByIndex(viewIndex, lView);
if (!(childComponentLView[FLAGS] & LViewFlags.CheckAlways)) {
Expand Down Expand Up @@ -1067,6 +1067,7 @@ export function instantiateRootComponent<T>(tView: TView, lView: LView, def: Com
directiveIndex, rootTNode.directiveStart,
'Because this is a root component the allocated expando should match the TNode component.');
configureViewWithDirective(tView, rootTNode, lView, directiveIndex, def);
initializeInputAndOutputAliases(tView, rootTNode);
}
const directive =
getNodeInjectable(lView, tView, rootTNode.directiveStart, rootTNode as TElementNode);
Expand Down
Expand Up @@ -932,6 +932,9 @@
{
"name": "initTNodeFlags"
},
{
"name": "initializeInputAndOutputAliases"
},
{
"name": "injectArgs"
},
Expand Down Expand Up @@ -1085,6 +1088,9 @@
{
"name": "markAsComponentHost"
},
{
"name": "markDirtyIfOnPush"
},
{
"name": "maybeWrapInNotSelector"
},
Expand Down
Expand Up @@ -698,6 +698,9 @@
{
"name": "initTNodeFlags"
},
{
"name": "initializeInputAndOutputAliases"
},
{
"name": "injectArgs"
},
Expand Down Expand Up @@ -965,6 +968,9 @@
{
"name": "setInjectImplementation"
},
{
"name": "setInputsForProperty"
},
{
"name": "setInputsFromAttrs"
},
Expand Down
Expand Up @@ -1028,6 +1028,9 @@
{
"name": "initTNodeFlags"
},
{
"name": "initializeInputAndOutputAliases"
},
{
"name": "injectArgs"
},
Expand Down Expand Up @@ -1199,6 +1202,9 @@
{
"name": "markAsComponentHost"
},
{
"name": "markDirtyIfOnPush"
},
{
"name": "markDuplicates"
},
Expand Down
Expand Up @@ -992,6 +992,9 @@
{
"name": "initTNodeFlags"
},
{
"name": "initializeInputAndOutputAliases"
},
{
"name": "injectArgs"
},
Expand Down Expand Up @@ -1160,6 +1163,9 @@
{
"name": "markAsComponentHost"
},
{
"name": "markDirtyIfOnPush"
},
{
"name": "markDuplicates"
},
Expand Down
Expand Up @@ -398,6 +398,12 @@
{
"name": "forwardRef"
},
{
"name": "generateInitialInputs"
},
{
"name": "generatePropertyAliases"
},
{
"name": "getClosureSafeProperty"
},
Expand Down Expand Up @@ -548,6 +554,9 @@
{
"name": "isImportedNgModuleProviders"
},
{
"name": "isInlineTemplate"
},
{
"name": "isLContainer"
},
Expand Down
6 changes: 6 additions & 0 deletions packages/core/test/bundling/router/bundle.golden_symbols.json
Expand Up @@ -1367,6 +1367,9 @@
{
"name": "initTNodeFlags"
},
{
"name": "initializeInputAndOutputAliases"
},
{
"name": "inject"
},
Expand Down Expand Up @@ -1811,6 +1814,9 @@
{
"name": "setInjectImplementation"
},
{
"name": "setInputsForProperty"
},
{
"name": "setInputsFromAttrs"
},
Expand Down
Expand Up @@ -473,6 +473,12 @@
{
"name": "forwardRef"
},
{
"name": "generateInitialInputs"
},
{
"name": "generatePropertyAliases"
},
{
"name": "getClosureSafeProperty"
},
Expand Down Expand Up @@ -632,6 +638,9 @@
{
"name": "isImportedNgModuleProviders"
},
{
"name": "isInlineTemplate"
},
{
"name": "isLContainer"
},
Expand Down
6 changes: 6 additions & 0 deletions packages/core/test/bundling/todo/bundle.golden_symbols.json
Expand Up @@ -866,6 +866,9 @@
{
"name": "initTNodeFlags"
},
{
"name": "initializeInputAndOutputAliases"
},
{
"name": "injectArgs"
},
Expand Down Expand Up @@ -1013,6 +1016,9 @@
{
"name": "markAsComponentHost"
},
{
"name": "markDirtyIfOnPush"
},
{
"name": "markDuplicates"
},
Expand Down

0 comments on commit 96c6139

Please sign in to comment.