Skip to content

Commit

Permalink
feat(core): add ability to set inputs on ComponentRef
Browse files Browse the repository at this point in the history
This change adds the setInput method to the ComponentRef. This
has two benefits:
- it takes input aliasing into account
- it marks OnPush components as dirty
- it triggers NgOnChanges lifecycle hook

Closes #12313
  • Loading branch information
pkozlowski-opensource committed Jun 30, 2022
1 parent e8c7dd1 commit 948c4ad
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 8 deletions.
3 changes: 3 additions & 0 deletions packages/core/src/linker/component_factory.ts
Expand Up @@ -23,6 +23,9 @@ import {ViewRef} from './view_ref';
* @publicApi
*/
export abstract class ComponentRef<C> {
// TODO(pk): document
abstract setInput(name: string, value: unknown): void;

/**
* The host or anchor [element](guide/glossary#element) for this component instance.
*/
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/render3/component.ts
Expand Up @@ -18,7 +18,7 @@ import {getComponentDef} from './definition';
import {diPublicInInjector, getOrCreateNodeInjectorForNode} from './di';
import {throwProviderNotFoundError} from './errors_di';
import {registerPostOrderHooks} from './hooks';
import {addToViewTree, CLEAN_PROMISE, createLView, createTView, getOrCreateTComponentView, getOrCreateTNode, initTNodeFlags, instantiateRootComponent, invokeHostBindingsInCreationMode, locateHostElement, markAsComponentHost, refreshView, registerHostBindingOpCodes, renderView} from './instructions/shared';
import {addToViewTree, CLEAN_PROMISE, createLView, createTView, getOrCreateTComponentView, getOrCreateTNode, initializeInputAndOutputAliases, initTNodeFlags, instantiateRootComponent, invokeHostBindingsInCreationMode, locateHostElement, markAsComponentHost, refreshView, registerHostBindingOpCodes, renderView} from './instructions/shared';
import {ComponentDef, ComponentType, RenderFlags} from './interfaces/definition';
import {TElementNode, TNodeType} from './interfaces/node';
import {PlayerHandler} from './interfaces/player';
Expand Down Expand Up @@ -206,6 +206,7 @@ export function createRootComponentView(
if (tView.firstCreatePass) {
diPublicInInjector(getOrCreateNodeInjectorForNode(tNode, rootView), tView, def.type);
markAsComponentHost(tView, tNode);
initializeInputAndOutputAliases(tView, tNode);
initTNodeFlags(tNode, rootView.length, 1);
}

Expand Down
16 changes: 13 additions & 3 deletions packages/core/src/render3/component_ref.ts
Expand Up @@ -25,12 +25,12 @@ 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 {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 {domRendererFactory3, RendererFactory3} from './interfaces/renderer';
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';
Expand Down Expand Up @@ -220,6 +220,7 @@ 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]);
initializeInputAndOutputAliases(rootTView, tElementNode);

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

override setInput(name: string, value: unknown): void {
let inputData = this._tNode.inputs;
let dataValue: PropertyAliasValue|undefined;
if (inputData != null && (dataValue = inputData[name])) {
setInputsForProperty(this._rootLView[TVIEW], this._rootLView, dataValue, name, value);
markDirtyIfOnPush(this._rootLView, this._tNode.index);
}
}

override get injector(): Injector {
return new NodeInjector(this._tNode, this._rootLView);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/render3/instructions/shared.ts
Expand Up @@ -936,7 +936,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 @@ -1039,7 +1039,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
73 changes: 71 additions & 2 deletions packages/core/test/render3/component_ref_spec.ts
Expand Up @@ -7,11 +7,12 @@
*/

import {RElement} from '@angular/core/src/render3/interfaces/renderer_dom';
import {TestBed} from '@angular/core/testing';

import {Component, Injector, Input, NgModuleRef, Output, RendererType2, ViewEncapsulation} from '../../src/core';
import {ChangeDetectionStrategy, Component, Injector, Input, NgModuleRef, OnChanges, Output, RendererType2, SimpleChanges, Type, ViewEncapsulation} from '../../src/core';
import {ComponentFactory} from '../../src/linker/component_factory';
import {Renderer2, RendererFactory2} from '../../src/render/api';
import {injectComponentFactoryResolver} from '../../src/render3/component_ref';
import {ComponentFactoryResolver, injectComponentFactoryResolver} from '../../src/render3/component_ref';
import {Sanitizer} from '../../src/sanitization/sanitizer';

import {MockRendererFactory} from './instructions/mock_renderer_factory';
Expand Down Expand Up @@ -280,4 +281,72 @@ describe('ComponentFactory', () => {
expect(hostNode.className).toEqual('HOST_COMPONENT HOST_RENDERER');
});
});

describe('setInput', () => {
it('should allow setting inputs on the ComponentRef', () => {
const inputChangesLog: string[] = [];

@Component({template: `{{in}}`})
class DynamicCmp implements OnChanges {
ngOnChanges(changes: SimpleChanges): void {
const inChange = changes['in'];
inputChangesLog.push(
`${inChange.previousValue}:${inChange.currentValue}:${inChange.firstChange}`);
}

@Input() in : string|undefined;
}

const fixture = TestBed.createComponent(DynamicCmp);

fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('');
expect(inputChangesLog).toEqual([]);

fixture.componentRef.setInput('in', 'first');
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('first');
expect(inputChangesLog).toEqual(['undefined:first:true']);

fixture.componentRef.setInput('in', 'second');
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('second');
expect(inputChangesLog).toEqual(['undefined:first:true', 'first:second:false']);
});

it('should allow setting mapped inputs on the ComponentRef', () => {
@Component({template: `{{in}}`})
class DynamicCmp {
@Input('publicName') in : string|undefined;
}

const fixture = TestBed.createComponent(DynamicCmp);

fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('');

fixture.componentRef.setInput('publicName', 'in value');
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('in value');
});

it('should mark components for check when setting an input on a ComponentRef', () => {
@Component({
template: `{{in}}`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
class DynamicCmp {
@Input() in : string|undefined;
}

const fixture = TestBed.createComponent(DynamicCmp);

fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('');

fixture.componentRef.setInput('in', 'pushed');
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('pushed');
});
});
});

0 comments on commit 948c4ad

Please sign in to comment.