diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts index 29af6e7523..7340722af2 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts @@ -49,6 +49,7 @@ export function getConstructorDependencies( let token = valueReferenceToExpression(param.typeValueReference, defaultImportRecorder); let optional = false, self = false, skipSelf = false, host = false; let resolved = R3ResolvedDependencyType.Token; + (param.decorators || []).filter(dec => isCore || isAngularCore(dec)).forEach(dec => { const name = isCore || dec.import === null ? dec.name : dec.import !.name; if (name === 'Inject') { @@ -79,6 +80,11 @@ export function getConstructorDependencies( ErrorCode.DECORATOR_UNEXPECTED, dec.node, `Unexpected decorator ${name} on parameter.`); } }); + + if (token instanceof ExternalExpr && token.value.name === 'ChangeDetectorRef' && + token.value.moduleName === '@angular/core') { + resolved = R3ResolvedDependencyType.ChangeDetectorRef; + } if (token === null) { errors.push({ index: idx, diff --git a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts index fc3581ad51..2f225c43e2 100644 --- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts @@ -2145,6 +2145,67 @@ describe('compiler compliance', () => { expectEmit(source, MyAppDefinition, 'Invalid MyApp definition'); }); + + it('should generate the proper instruction when injecting ChangeDetectorRef into a pipe', + () => { + + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule, Pipe, PipeTransform, ChangeDetectorRef, Optional} from '@angular/core'; + + @Pipe({name: 'myPipe'}) + export class MyPipe implements PipeTransform { + constructor(changeDetectorRef: ChangeDetectorRef) {} + + transform(value: any, ...args: any[]) { return value; } + } + + @Pipe({name: 'myOtherPipe'}) + export class MyOtherPipe implements PipeTransform { + constructor(@Optional() changeDetectorRef: ChangeDetectorRef) {} + + transform(value: any, ...args: any[]) { return value; } + } + + @Component({ + selector: 'my-app', + template: '{{name | myPipe }}

{{ name | myOtherPipe }}

' + }) + export class MyApp { + name = 'World'; + } + + @NgModule({declarations:[MyPipe, MyOtherPipe, MyApp]}) + export class MyModule {} + ` + } + }; + + const MyPipeDefinition = ` + MyPipe.ngPipeDef = $r3$.ɵɵdefinePipe({ + name: "myPipe", + type: MyPipe, + factory: function MyPipe_Factory(t) { return new (t || MyPipe)($r3$.ɵɵinjectPipeChangeDetectorRef()); }, + pure: true + }); + `; + + const MyOtherPipeDefinition = ` + MyOtherPipe.ngPipeDef = $r3$.ɵɵdefinePipe({ + name: "myOtherPipe", + type: MyOtherPipe, + factory: function MyOtherPipe_Factory(t) { return new (t || MyOtherPipe)($r3$.ɵɵinjectPipeChangeDetectorRef(8)); }, + pure: true + });`; + + const result = compile(files, angularFiles); + const source = result.source; + + expectEmit(source, MyPipeDefinition, 'Invalid pipe definition'); + expectEmit(source, MyOtherPipeDefinition, 'Invalid alternate pipe definition'); + }); + }); it('local reference', () => { diff --git a/packages/compiler/src/compiler_facade_interface.ts b/packages/compiler/src/compiler_facade_interface.ts index 1564d28fe3..6f6a0099b6 100644 --- a/packages/compiler/src/compiler_facade_interface.ts +++ b/packages/compiler/src/compiler_facade_interface.ts @@ -65,6 +65,7 @@ export type Provider = any; export enum R3ResolvedDependencyType { Token = 0, Attribute = 1, + ChangeDetectorRef = 2, } export interface R3DependencyMetadataFacade { diff --git a/packages/compiler/src/render3/r3_factory.ts b/packages/compiler/src/render3/r3_factory.ts index 4808322ef4..87b7edb6c3 100644 --- a/packages/compiler/src/render3/r3_factory.ts +++ b/packages/compiler/src/render3/r3_factory.ts @@ -97,6 +97,11 @@ export enum R3ResolvedDependencyType { * The token expression is a string representing the attribute name. */ Attribute = 1, + + /** + * Injecting the `ChangeDetectorRef` token. Needs special handling when injected into a pipe. + */ + ChangeDetectorRef = 2, } /** @@ -138,8 +143,8 @@ export interface R3DependencyMetadata { /** * Construct a factory function expression for the given `R3FactoryMetadata`. */ -export function compileFactoryFunction(meta: R3FactoryMetadata): - {factory: o.Expression, statements: o.Statement[]} { +export function compileFactoryFunction( + meta: R3FactoryMetadata, isPipe = false): {factory: o.Expression, statements: o.Statement[]} { const t = o.variable('t'); const statements: o.Statement[] = []; @@ -155,7 +160,8 @@ export function compileFactoryFunction(meta: R3FactoryMetadata): if (meta.deps !== null) { // There is a constructor (either explicitly or implicitly defined). if (meta.deps !== 'invalid') { - ctorExpr = new o.InstantiateExpr(typeForCtor, injectDependencies(meta.deps, meta.injectFn)); + ctorExpr = + new o.InstantiateExpr(typeForCtor, injectDependencies(meta.deps, meta.injectFn, isPipe)); } } else { const baseFactory = o.variable(`ɵ${meta.name}_BaseFactory`); @@ -203,7 +209,7 @@ export function compileFactoryFunction(meta: R3FactoryMetadata): } else if (isDelegatedMetadata(meta)) { // This type is created with a delegated factory. If a type parameter is not specified, call // the factory instead. - const delegateArgs = injectDependencies(meta.delegateDeps, meta.injectFn); + const delegateArgs = injectDependencies(meta.delegateDeps, meta.injectFn, isPipe); // Either call `new delegate(...)` or `delegate(...)` depending on meta.useNewForDelegate. const factoryExpr = new ( meta.delegateType === R3FactoryDelegateType.Class ? @@ -232,30 +238,38 @@ export function compileFactoryFunction(meta: R3FactoryMetadata): } function injectDependencies( - deps: R3DependencyMetadata[], injectFn: o.ExternalReference): o.Expression[] { - return deps.map(dep => compileInjectDependency(dep, injectFn)); + deps: R3DependencyMetadata[], injectFn: o.ExternalReference, isPipe: boolean): o.Expression[] { + return deps.map(dep => compileInjectDependency(dep, injectFn, isPipe)); } function compileInjectDependency( - dep: R3DependencyMetadata, injectFn: o.ExternalReference): o.Expression { + dep: R3DependencyMetadata, injectFn: o.ExternalReference, isPipe: boolean): o.Expression { // Interpret the dependency according to its resolved type. switch (dep.resolved) { - case R3ResolvedDependencyType.Token: { + case R3ResolvedDependencyType.Token: + case R3ResolvedDependencyType.ChangeDetectorRef: // Build up the injection flags according to the metadata. const flags = InjectFlags.Default | (dep.self ? InjectFlags.Self : 0) | (dep.skipSelf ? InjectFlags.SkipSelf : 0) | (dep.host ? InjectFlags.Host : 0) | (dep.optional ? InjectFlags.Optional : 0); - // Build up the arguments to the injectFn call. - const injectArgs = [dep.token]; // If this dependency is optional or otherwise has non-default flags, then additional // parameters describing how to inject the dependency must be passed to the inject function // that's being used. - if (flags !== InjectFlags.Default || dep.optional) { - injectArgs.push(o.literal(flags)); + let flagsParam: o.LiteralExpr|null = + (flags !== InjectFlags.Default || dep.optional) ? o.literal(flags) : null; + + // We have a separate instruction for injecting ChangeDetectorRef into a pipe. + if (isPipe && dep.resolved === R3ResolvedDependencyType.ChangeDetectorRef) { + return o.importExpr(R3.injectPipeChangeDetectorRef).callFn(flagsParam ? [flagsParam] : []); + } + + // Build up the arguments to the injectFn call. + const injectArgs = [dep.token]; + if (flagsParam) { + injectArgs.push(flagsParam); } return o.importExpr(injectFn).callFn(injectArgs); - } case R3ResolvedDependencyType.Attribute: // In the case of attributes, the attribute name in question is given as the token. return o.importExpr(R3.injectAttribute).callFn([dep.token]); diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 014896f677..a9002d9d70 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -215,6 +215,9 @@ export class Identifiers { static injectAttribute: o.ExternalReference = {name: 'ɵɵinjectAttribute', moduleName: CORE}; + static injectPipeChangeDetectorRef: + o.ExternalReference = {name: 'ɵɵinjectPipeChangeDetectorRef', moduleName: CORE}; + static directiveInject: o.ExternalReference = {name: 'ɵɵdirectiveInject', moduleName: CORE}; static templateRefExtractor: diff --git a/packages/compiler/src/render3/r3_pipe_compiler.ts b/packages/compiler/src/render3/r3_pipe_compiler.ts index 5505ccbcbf..2ebf1f3748 100644 --- a/packages/compiler/src/render3/r3_pipe_compiler.ts +++ b/packages/compiler/src/render3/r3_pipe_compiler.ts @@ -57,12 +57,14 @@ export function compilePipeFromMetadata(metadata: R3PipeMetadata) { // e.g. `type: MyPipe` definitionMapValues.push({key: 'type', value: metadata.type, quoted: false}); - const templateFactory = compileFactoryFunction({ - name: metadata.name, - type: metadata.type, - deps: metadata.deps, - injectFn: R3.directiveInject, - }); + const templateFactory = compileFactoryFunction( + { + name: metadata.name, + type: metadata.type, + deps: metadata.deps, + injectFn: R3.directiveInject, + }, + true); definitionMapValues.push({key: 'factory', value: templateFactory.factory, quoted: false}); // e.g. `pure: true` diff --git a/packages/core/src/compiler/compiler_facade_interface.ts b/packages/core/src/compiler/compiler_facade_interface.ts index 1564d28fe3..6f6a0099b6 100644 --- a/packages/core/src/compiler/compiler_facade_interface.ts +++ b/packages/core/src/compiler/compiler_facade_interface.ts @@ -65,6 +65,7 @@ export type Provider = any; export enum R3ResolvedDependencyType { Token = 0, Attribute = 1, + ChangeDetectorRef = 2, } export interface R3DependencyMetadataFacade { diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index 7e57dd9248..e2401de524 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -33,6 +33,7 @@ export { RenderFlags as ɵRenderFlags, ɵɵdirectiveInject, ɵɵinjectAttribute, + ɵɵinjectPipeChangeDetectorRef, ɵɵgetFactoryOf, ɵɵgetInheritedFactory, ɵɵsetComponentScope, diff --git a/packages/core/src/di/jit/util.ts b/packages/core/src/di/jit/util.ts index cc4805aa9d..51270cc21c 100644 --- a/packages/core/src/di/jit/util.ts +++ b/packages/core/src/di/jit/util.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {ChangeDetectorRef} from '../../change_detection/change_detector_ref'; import {CompilerFacade, R3DependencyMetadataFacade, getCompilerFacade} from '../../compiler/compiler_facade'; import {Type} from '../../interface/type'; import {ReflectionCapabilities} from '../../reflection/reflection_capabilities'; @@ -66,6 +67,9 @@ function reflectDependency(compiler: CompilerFacade, dep: any | any[]): R3Depend } meta.token = param.attributeName; meta.resolved = compiler.R3ResolvedDependencyType.Attribute; + } else if (param === ChangeDetectorRef) { + meta.token = param; + meta.resolved = compiler.R3ResolvedDependencyType.ChangeDetectorRef; } else { setTokenAndResolvedType(param); } diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index 0c08142c5a..d481ef1290 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -6,10 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -import {InjectFlags, InjectionToken} from '../di'; +import {InjectionToken} from '../di/injection_token'; import {Injector} from '../di/injector'; import {injectRootLimpMode, setInjectImplementation} from '../di/injector_compatibility'; import {getInjectableDef, getInjectorDef} from '../di/interface/defs'; +import {InjectFlags} from '../di/interface/injector'; import {Type} from '../interface/type'; import {assertDefined, assertEqual} from '../util/assert'; diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 863c1cd7da..7edb7c7ed3 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -198,7 +198,7 @@ export { ɵɵpureFunctionV, } from './pure_function'; -export {ɵɵtemplateRefExtractor} from './view_engine_compatibility_prebound'; +export {ɵɵtemplateRefExtractor, ɵɵinjectPipeChangeDetectorRef} from './view_engine_compatibility_prebound'; export {ɵɵresolveWindow, ɵɵresolveDocument, ɵɵresolveBody} from './util/misc_utils'; diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index 3d34f58e3e..888a9dd7fe 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -42,6 +42,7 @@ export const angularCoreEnv: {[name: string]: Function} = 'ɵɵgetInheritedFactory': r3.ɵɵgetInheritedFactory, 'ɵɵinject': ɵɵinject, 'ɵɵinjectAttribute': r3.ɵɵinjectAttribute, + 'ɵɵinjectPipeChangeDetectorRef': r3.ɵɵinjectPipeChangeDetectorRef, 'ɵɵtemplateRefExtractor': r3.ɵɵtemplateRefExtractor, 'ɵɵNgOnChangesFeature': r3.ɵɵNgOnChangesFeature, 'ɵɵProvidersFeature': r3.ɵɵProvidersFeature, diff --git a/packages/core/src/render3/view_engine_compatibility.ts b/packages/core/src/render3/view_engine_compatibility.ts index b35ef17216..7ac384a845 100644 --- a/packages/core/src/render3/view_engine_compatibility.ts +++ b/packages/core/src/render3/view_engine_compatibility.ts @@ -362,8 +362,8 @@ export function createContainerRef( /** Returns a ChangeDetectorRef (a.k.a. a ViewRef) */ -export function injectChangeDetectorRef(): ViewEngine_ChangeDetectorRef { - return createViewRef(getPreviousOrParentTNode(), getLView(), null); +export function injectChangeDetectorRef(isPipe = false): ViewEngine_ChangeDetectorRef { + return createViewRef(getPreviousOrParentTNode(), getLView(), isPipe); } /** @@ -371,15 +371,15 @@ export function injectChangeDetectorRef(): ViewEngine_ChangeDetectorRef { * * @param hostTNode The node that is requesting a ChangeDetectorRef * @param hostView The view to which the node belongs - * @param context The context for this change detector ref + * @param isPipe Whether the view is being injected into a pipe. * @returns The ChangeDetectorRef to use */ -export function createViewRef( - hostTNode: TNode, hostView: LView, context: any): ViewEngine_ChangeDetectorRef { - if (isComponent(hostTNode)) { +function createViewRef( + hostTNode: TNode, hostView: LView, isPipe: boolean): ViewEngine_ChangeDetectorRef { + if (isComponent(hostTNode) && !isPipe) { const componentIndex = hostTNode.directiveStart; const componentView = getComponentViewByIndex(hostTNode.index, hostView); - return new ViewRef(componentView, context, componentIndex); + return new ViewRef(componentView, null, componentIndex); } else if ( hostTNode.type === TNodeType.Element || hostTNode.type === TNodeType.Container || hostTNode.type === TNodeType.ElementContainer) { diff --git a/packages/core/src/render3/view_engine_compatibility_prebound.ts b/packages/core/src/render3/view_engine_compatibility_prebound.ts index 2c5505a831..06472bcf52 100644 --- a/packages/core/src/render3/view_engine_compatibility_prebound.ts +++ b/packages/core/src/render3/view_engine_compatibility_prebound.ts @@ -7,12 +7,14 @@ */ +import {ChangeDetectorRef} from '../change_detection/change_detector_ref'; +import {InjectFlags} from '../di/interface/injector'; import {ElementRef as ViewEngine_ElementRef} from '../linker/element_ref'; import {TemplateRef as ViewEngine_TemplateRef} from '../linker/template_ref'; import {TNode} from './interfaces/node'; import {LView} from './interfaces/view'; -import {createTemplateRef} from './view_engine_compatibility'; +import {createTemplateRef, injectChangeDetectorRef} from './view_engine_compatibility'; @@ -25,3 +27,18 @@ import {createTemplateRef} from './view_engine_compatibility'; export function ɵɵtemplateRefExtractor(tNode: TNode, currentView: LView) { return createTemplateRef(ViewEngine_TemplateRef, ViewEngine_ElementRef, tNode, currentView); } + + +/** + * Returns the appropriate `ChangeDetectorRef` for a pipe. + * + * @codeGenApi + */ +export function ɵɵinjectPipeChangeDetectorRef(flags = InjectFlags.Default): ChangeDetectorRef|null { + const value = injectChangeDetectorRef(true); + if (value == null && !(flags & InjectFlags.Optional)) { + throw new Error(`No provider for ChangeDetectorRef!`); + } else { + return value; + } +} diff --git a/packages/core/test/acceptance/pipe_spec.ts b/packages/core/test/acceptance/pipe_spec.ts index 56efdd827c..d58a7c5169 100644 --- a/packages/core/test/acceptance/pipe_spec.ts +++ b/packages/core/test/acceptance/pipe_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, Directive, Inject, Injectable, InjectionToken, Input, NgModule, OnDestroy, Pipe, PipeTransform, ViewChild} from '@angular/core'; +import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Directive, Inject, Injectable, InjectionToken, Input, NgModule, OnDestroy, Pipe, PipeTransform, ViewChild} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {expect} from '@angular/platform-browser/testing/src/matchers'; @@ -398,6 +398,108 @@ describe('pipe', () => { expect(fixture.nativeElement.textContent).toBe('MyComponent Title - Service Title'); }); + it('should inject the ChangeDetectorRef of the containing view when using pipe inside a component input', + () => { + let pipeChangeDetectorRef: ChangeDetectorRef|undefined; + + @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'some-comp', + template: 'Inner value: "{{displayValue}}"', + }) + class SomeComp { + @Input() value: any; + displayValue = 0; + } + + @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + Outer value: "{{displayValue}}" + `, + }) + class App { + @Input() something: any; + @ViewChild(SomeComp, {static: false}) comp !: SomeComp; + pipeValue = 10; + displayValue = 0; + } + + @Pipe({name: 'testPipe'}) + class TestPipe implements PipeTransform { + constructor(changeDetectorRef: ChangeDetectorRef) { + pipeChangeDetectorRef = changeDetectorRef; + } + + transform() { return ''; } + } + + TestBed.configureTestingModule({declarations: [App, SomeComp, TestPipe]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + fixture.componentInstance.displayValue = 1; + fixture.componentInstance.comp.displayValue = 1; + pipeChangeDetectorRef !.markForCheck(); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toContain('Outer value: "1"'); + expect(fixture.nativeElement.textContent).toContain('Inner value: "0"'); + }); + + it('should inject the ChangeDetectorRef of the containing view when using pipe inside a component input which has child nodes', + () => { + let pipeChangeDetectorRef: ChangeDetectorRef|undefined; + + @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'some-comp', + template: 'Inner value: "{{displayValue}}" ', + }) + class SomeComp { + @Input() value: any; + displayValue = 0; + } + + @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + +
Hello
+
+ Outer value: "{{displayValue}}" + `, + }) + class App { + @Input() something: any; + @ViewChild(SomeComp, {static: false}) comp !: SomeComp; + pipeValue = 10; + displayValue = 0; + } + + @Pipe({name: 'testPipe'}) + class TestPipe implements PipeTransform { + constructor(changeDetectorRef: ChangeDetectorRef) { + pipeChangeDetectorRef = changeDetectorRef; + } + + transform() { return ''; } + } + + TestBed.configureTestingModule({declarations: [App, SomeComp, TestPipe]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + fixture.componentInstance.displayValue = 1; + fixture.componentInstance.comp.displayValue = 1; + pipeChangeDetectorRef !.markForCheck(); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toContain('Outer value: "1"'); + expect(fixture.nativeElement.textContent).toContain('Inner value: "0"'); + }); + }); }); diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index d3813ae88f..89aa8fd70e 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -920,6 +920,8 @@ export interface ɵɵInjectorDef { providers: (Type | ValueProvider | ExistingProvider | FactoryProvider | ConstructorProvider | StaticClassProvider | ClassProvider | any[])[]; } +export declare function ɵɵinjectPipeChangeDetectorRef(flags?: InjectFlags): ChangeDetectorRef | null; + export declare function ɵɵlistener(eventName: string, listenerFn: (e?: any) => any, useCapture?: boolean, eventTargetResolver?: GlobalTargetResolver): void; export declare function ɵɵload(index: number): T;