From 949fca566c34cac45e0ffab84dbe109bc81018ce Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Wed, 3 Nov 2021 19:42:02 +0000 Subject: [PATCH] fix(compiler): ensure that partially compiled queries can handle forward references When a partially compiled component or directive is "linked" in JIT mode, the body of its declaration is evaluated by the JavaScript runtime. If a class is referenced in a query (e.g. `ViewQuery` or `ContentQuery`) but its definition is later in the file, then the reference must be wrapped in a `forwardRef()` call. Previously, query predicates were not wrapped correctly in partial declarations causing the code to crash at runtime. In AOT mode, this code is never evaluated but instead transformed as part of the build, so this bug did not become apparent until Angular Material started running JIT mode tests on its distributable output. This change fixes this problem by noting when queries are wrapped in `forwardRef()` calls and ensuring that this gets passed through to partial compilation declarations and then suitably stripped during linking. See https://github.com/angular/components/pull/23882 and https://github.com/angular/components/issues/23907 --- .../partial_directive_linker_1.ts | 6 + .../src/ngtsc/annotations/src/directive.ts | 5 +- .../queries/GOLDEN_PARTIAL.js | 176 ++++++++++++++++++ .../queries/TEST_CASES.json | 28 +++ .../queries/content_query_forward_ref.js | 27 +++ .../queries/content_query_forward_ref.ts | 34 ++++ .../queries/view_query_forward_ref.js | 16 ++ .../queries/view_query_forward_ref.ts | 32 ++++ .../compiler/src/compiler_facade_interface.ts | 1 + packages/compiler/src/jit_compiler_facade.ts | 6 +- packages/compiler/src/render3/partial/api.ts | 10 +- .../compiler/src/render3/partial/directive.ts | 18 +- packages/compiler/src/render3/view/api.ts | 5 + .../src/compiler/compiler_facade_interface.ts | 1 + .../render3/jit/declare_directive_spec.ts | 25 ++- 15 files changed, 381 insertions(+), 9 deletions(-) create mode 100644 packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/queries/content_query_forward_ref.js create mode 100644 packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/queries/content_query_forward_ref.ts create mode 100644 packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/queries/view_query_forward_ref.js create mode 100644 packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/queries/view_query_forward_ref.ts diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_directive_linker_1.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_directive_linker_1.ts index 9985841f6f8761..94746e4753b701 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_directive_linker_1.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_directive_linker_1.ts @@ -139,6 +139,12 @@ function toQueryMetadata(obj: AstObject entry.getString()); + } else if ( + obj.has('isForwardRef') && obj.getBoolean('isForwardRef') && + predicateExpr.isCallExpression()) { + const forwardRefArg = predicateExpr.getArguments()[0] as AstValue; + predicate = forwardRefArg.getFunctionReturnValue().getOpaque(); + } else { predicate = predicateExpr.getOpaque(); } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index 297f36dbb67b55..4d08367c49c5f5 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -531,7 +531,9 @@ export function extractQueryMetadata( ErrorCode.DECORATOR_ARITY_WRONG, exprNode, `@${name} must have arguments`); } const first = name === 'ViewChild' || name === 'ContentChild'; - const node = tryUnwrapForwardRef(args[0], reflector) ?? args[0]; + const forwardReferenceTarget = tryUnwrapForwardRef(args[0], reflector); + const node = forwardReferenceTarget ?? args[0]; + const arg = evaluator.evaluate(node); /** Whether or not this query should collect only static results (see view/api.ts) */ @@ -606,6 +608,7 @@ export function extractQueryMetadata( return { propertyName, predicate, + isForwardRef: forwardReferenceTarget !== null, first, descendants, read, diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/queries/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/queries/GOLDEN_PARTIAL.js index 7343f20be71d7d..66e2238941382d 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/queries/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/queries/GOLDEN_PARTIAL.js @@ -79,6 +79,92 @@ export declare class MyModule { static ɵinj: i0.ɵɵInjectorDeclaration; } +/**************************************************************************************************** + * PARTIAL FILE: view_query_forward_ref.js + ****************************************************************************************************/ +import { Component, Directive, forwardRef, NgModule, ViewChild, ViewChildren } from '@angular/core'; +import * as i0 from "@angular/core"; +export class ViewQueryComponent { +} +ViewQueryComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ViewQueryComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); +ViewQueryComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", type: ViewQueryComponent, selector: "view-query-component", viewQueries: [{ propertyName: "someDir", first: true, predicate: i0.forwardRef(function () { return SomeDirective; }), isForwardRef: true, descendants: true }, { propertyName: "someDirList", predicate: i0.forwardRef(function () { return SomeDirective; }), isForwardRef: true, descendants: true }], ngImport: i0, template: ` +
+ `, isInline: true, directives: [{ type: i0.forwardRef(function () { return SomeDirective; }), selector: "[someDir]" }] }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ViewQueryComponent, decorators: [{ + type: Component, + args: [{ + selector: 'view-query-component', + template: ` +
+ ` + }] + }], propDecorators: { someDir: [{ + type: ViewChild, + args: [forwardRef(() => SomeDirective)] + }], someDirList: [{ + type: ViewChildren, + args: [forwardRef(() => SomeDirective)] + }] } }); +export class MyApp { +} +MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component }); +MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, selector: "my-app", ngImport: i0, template: ` + + `, isInline: true, components: [{ type: ViewQueryComponent, selector: "view-query-component" }] }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{ + type: Component, + args: [{ + selector: 'my-app', + template: ` + + ` + }] + }] }); +export class SomeDirective { +} +SomeDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SomeDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); +SomeDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", type: SomeDirective, selector: "[someDir]", ngImport: i0 }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SomeDirective, decorators: [{ + type: Directive, + args: [{ + selector: '[someDir]', + }] + }] }); +export class MyModule { +} +MyModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); +MyModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, declarations: [SomeDirective, ViewQueryComponent, MyApp] }); +MyModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, decorators: [{ + type: NgModule, + args: [{ declarations: [SomeDirective, ViewQueryComponent, MyApp] }] + }] }); + +/**************************************************************************************************** + * PARTIAL FILE: view_query_forward_ref.d.ts + ****************************************************************************************************/ +import { QueryList } from '@angular/core'; +import * as i0 from "@angular/core"; +export declare class ViewQueryComponent { + someDir: SomeDirective; + someDirList: QueryList; + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} +export declare class MyApp { + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} +export declare class SomeDirective { + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; +} +export declare class MyModule { + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵmod: i0.ɵɵNgModuleDeclaration; + static ɵinj: i0.ɵɵInjectorDeclaration; +} + /**************************************************************************************************** * PARTIAL FILE: view_query_for_local_ref.js ****************************************************************************************************/ @@ -410,6 +496,96 @@ export declare class MyModule { static ɵinj: i0.ɵɵInjectorDeclaration; } +/**************************************************************************************************** + * PARTIAL FILE: content_query_forward_ref.js + ****************************************************************************************************/ +import { Component, ContentChild, ContentChildren, Directive, forwardRef, NgModule } from '@angular/core'; +import * as i0 from "@angular/core"; +export class ContentQueryComponent { +} +ContentQueryComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ContentQueryComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); +ContentQueryComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", type: ContentQueryComponent, selector: "content-query-component", queries: [{ propertyName: "someDir", first: true, predicate: i0.forwardRef(function () { return SomeDirective; }), isForwardRef: true, descendants: true }, { propertyName: "someDirList", predicate: i0.forwardRef(function () { return SomeDirective; }), isForwardRef: true }], ngImport: i0, template: ` +
+ `, isInline: true }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ContentQueryComponent, decorators: [{ + type: Component, + args: [{ + selector: 'content-query-component', + template: ` +
+ ` + }] + }], propDecorators: { someDir: [{ + type: ContentChild, + args: [forwardRef(() => SomeDirective)] + }], someDirList: [{ + type: ContentChildren, + args: [forwardRef(() => SomeDirective)] + }] } }); +export class MyApp { +} +MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component }); +MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, selector: "my-app", ngImport: i0, template: ` + +
+
+ `, isInline: true, components: [{ type: i0.forwardRef(function () { return ContentQueryComponent; }), selector: "content-query-component" }], directives: [{ type: i0.forwardRef(function () { return SomeDirective; }), selector: "[someDir]" }] }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{ + type: Component, + args: [{ + selector: 'my-app', + template: ` + +
+
+ ` + }] + }] }); +export class SomeDirective { +} +SomeDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SomeDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); +SomeDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", type: SomeDirective, selector: "[someDir]", ngImport: i0 }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: SomeDirective, decorators: [{ + type: Directive, + args: [{ + selector: '[someDir]', + }] + }] }); +export class MyModule { +} +MyModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); +MyModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, declarations: [SomeDirective, ContentQueryComponent, MyApp] }); +MyModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, decorators: [{ + type: NgModule, + args: [{ declarations: [SomeDirective, ContentQueryComponent, MyApp] }] + }] }); + +/**************************************************************************************************** + * PARTIAL FILE: content_query_forward_ref.d.ts + ****************************************************************************************************/ +import { QueryList } from '@angular/core'; +import * as i0 from "@angular/core"; +export declare class ContentQueryComponent { + someDir: SomeDirective; + someDirList: QueryList; + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} +export declare class MyApp { + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} +export declare class SomeDirective { + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; +} +export declare class MyModule { + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵmod: i0.ɵɵNgModuleDeclaration; + static ɵinj: i0.ɵɵInjectorDeclaration; +} + /**************************************************************************************************** * PARTIAL FILE: content_query_for_local_ref.js ****************************************************************************************************/ diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/queries/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/queries/TEST_CASES.json index a842eacb0ac43f..2a700b16531fbb 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/queries/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/queries/TEST_CASES.json @@ -16,6 +16,20 @@ } ] }, + { + "description": "should support view queries with forwardRefs", + "inputFiles": [ + "view_query_forward_ref.ts" + ], + "expectations": [ + { + "failureMessage": "Invalid viewQuery declaration", + "files": [ + "view_query_forward_ref.js" + ] + } + ] + }, { "description": "should support view queries with local refs", "inputFiles": [ @@ -75,6 +89,20 @@ } ] }, + { + "description": "should support content queries with forwardRefs", + "inputFiles": [ + "content_query_forward_ref.ts" + ], + "expectations": [ + { + "failureMessage": "Invalid contentQuery declaration", + "files": [ + "content_query_forward_ref.js" + ] + } + ] + }, { "description": "should support content queries with local refs", "inputFiles": [ diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/queries/content_query_forward_ref.js b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/queries/content_query_forward_ref.js new file mode 100644 index 00000000000000..d0c83d79331695 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/queries/content_query_forward_ref.js @@ -0,0 +1,27 @@ +ContentQueryComponent.ɵcmp = /*@__PURE__*/ $r3$.ɵɵdefineComponent({ + type: ContentQueryComponent, + selectors: [["content-query-component"]], + contentQueries: function ContentQueryComponent_ContentQueries(rf, ctx, dirIndex) { + if (rf & 1) { + $r3$.ɵɵcontentQuery(dirIndex, SomeDirective, 5); + $r3$.ɵɵcontentQuery(dirIndex, SomeDirective, 4); + } + if (rf & 2) { + let $tmp$; + $r3$.ɵɵqueryRefresh($tmp$ = $r3$.ɵɵloadQuery()) && (ctx.someDir = $tmp$.first); + $r3$.ɵɵqueryRefresh($tmp$ = $r3$.ɵɵloadQuery()) && (ctx.someDirList = $tmp$); + } + }, + ngContentSelectors: _c0, + decls: 2, + vars: 0, + template: function ContentQueryComponent_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵprojectionDef(); + $r3$.ɵɵelementStart(0, "div"); + $r3$.ɵɵprojection(1); + $r3$.ɵɵelementEnd(); + } + }, + encapsulation: 2 +}); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/queries/content_query_forward_ref.ts b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/queries/content_query_forward_ref.ts new file mode 100644 index 00000000000000..73fe0170ea8e58 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/queries/content_query_forward_ref.ts @@ -0,0 +1,34 @@ +import {Component, ContentChild, ContentChildren, Directive, forwardRef, NgModule, QueryList} from '@angular/core'; + +@Component({ + selector: 'content-query-component', + template: ` +
+ ` +}) +export class ContentQueryComponent { + @ContentChild(forwardRef(() => SomeDirective)) someDir!: SomeDirective; + @ContentChildren(forwardRef(() => SomeDirective)) someDirList!: QueryList; +} + +@Component({ + selector: 'my-app', + template: ` + +
+
+ ` +}) +export class MyApp { +} + + +@Directive({ + selector: '[someDir]', +}) +export class SomeDirective { +} + +@NgModule({declarations: [SomeDirective, ContentQueryComponent, MyApp]}) +export class MyModule { +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/queries/view_query_forward_ref.js b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/queries/view_query_forward_ref.js new file mode 100644 index 00000000000000..918fd29ffb16e5 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/queries/view_query_forward_ref.js @@ -0,0 +1,16 @@ +ViewQueryComponent.ɵcmp = /*@__PURE__*/ $r3$.ɵɵdefineComponent({ + type: ViewQueryComponent, + selectors: [["view-query-component"]], + viewQuery: function ViewQueryComponent_Query(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵviewQuery(SomeDirective, 5); + $r3$.ɵɵviewQuery(SomeDirective, 5); + } + if (rf & 2) { + let $tmp$; + $r3$.ɵɵqueryRefresh($tmp$ = $r3$.ɵɵloadQuery()) && (ctx.someDir = $tmp$.first); + $r3$.ɵɵqueryRefresh($tmp$ = $r3$.ɵɵloadQuery()) && (ctx.someDirList = $tmp$); + } + }, + // ... +}); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/queries/view_query_forward_ref.ts b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/queries/view_query_forward_ref.ts new file mode 100644 index 00000000000000..c6acd8409dbaad --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/queries/view_query_forward_ref.ts @@ -0,0 +1,32 @@ +import {Component, Directive, forwardRef, NgModule, QueryList, ViewChild, ViewChildren} from '@angular/core'; + +@Component({ + selector: 'view-query-component', + template: ` +
+ ` +}) +export class ViewQueryComponent { + @ViewChild(forwardRef(() => SomeDirective)) someDir!: SomeDirective; + @ViewChildren(forwardRef(() => SomeDirective)) someDirList!: QueryList; +} + +@Component({ + selector: 'my-app', + template: ` + + ` +}) +export class MyApp { +} + + +@Directive({ + selector: '[someDir]', +}) +export class SomeDirective { +} + +@NgModule({declarations: [SomeDirective, ViewQueryComponent, MyApp]}) +export class MyModule { +} diff --git a/packages/compiler/src/compiler_facade_interface.ts b/packages/compiler/src/compiler_facade_interface.ts index 6a27e8e1786e00..6c5b1c6051a1e1 100644 --- a/packages/compiler/src/compiler_facade_interface.ts +++ b/packages/compiler/src/compiler_facade_interface.ts @@ -278,6 +278,7 @@ export interface R3DeclareQueryMetadataFacade { propertyName: string; first?: boolean; predicate: OpaqueValue|string[]; + isForwardRef?: boolean; descendants?: boolean; read?: OpaqueValue; static?: boolean; diff --git a/packages/compiler/src/jit_compiler_facade.ts b/packages/compiler/src/jit_compiler_facade.ts index ff2ca97bc8f696..8ef646b3d0e352 100644 --- a/packages/compiler/src/jit_compiler_facade.ts +++ b/packages/compiler/src/jit_compiler_facade.ts @@ -26,6 +26,7 @@ import {compileComponentFromMetadata, compileDirectiveFromMetadata, ParsedHostBi import {makeBindingParser, parseTemplate} from './render3/view/template'; import {ResourceLoader} from './resource_loader'; import {DomElementSchemaRegistry} from './schema/dom_element_schema_registry'; +import {resolveForwardRef} from './util'; export class CompilerFacadeImpl implements CompilerFacade { FactoryTarget = FactoryTarget as any; @@ -306,8 +307,9 @@ function convertQueryDeclarationToMetadata(declaration: R3DeclareQueryMetadataFa return { propertyName: declaration.propertyName, first: declaration.first ?? false, - predicate: Array.isArray(declaration.predicate) ? declaration.predicate : - new WrappedNodeExpr(declaration.predicate), + predicate: Array.isArray(declaration.predicate) ? + declaration.predicate : + new WrappedNodeExpr(resolveForwardRef(declaration.predicate)), descendants: declaration.descendants ?? false, read: declaration.read ? new WrappedNodeExpr(declaration.read) : null, static: declaration.static ?? false, diff --git a/packages/compiler/src/render3/partial/api.ts b/packages/compiler/src/render3/partial/api.ts index a6ce0dcfedd3f6..f5a0ede16eef95 100644 --- a/packages/compiler/src/render3/partial/api.ts +++ b/packages/compiler/src/render3/partial/api.ts @@ -233,11 +233,17 @@ export interface R3DeclareQueryMetadata { first?: boolean; /** - * Either an expression representing a type or `InjectionToken` for the query - * predicate, or a set of string selectors. + * Either an expression representing a type (possibly wrapped in a `forwardRef()`) or + * `InjectionToken` for the query predicate, or a set of string selectors. */ predicate: o.Expression|string[]; + /** + * True if the `predicate` is wrapped in a `forwardRef()` invocation. + */ + isForwardRef: boolean; + + /** * Whether to include only direct children or all descendants. Defaults to false. */ diff --git a/packages/compiler/src/render3/partial/directive.ts b/packages/compiler/src/render3/partial/directive.ts index 310146963b69f3..d1e2d6480171e5 100644 --- a/packages/compiler/src/render3/partial/directive.ts +++ b/packages/compiler/src/render3/partial/directive.ts @@ -12,7 +12,7 @@ import {R3DirectiveMetadata, R3HostMetadata, R3QueryMetadata} from '../view/api' import {createDirectiveType} from '../view/compiler'; import {asLiteral, conditionallyCreateMapObjectLiteral, DefinitionMap} from '../view/util'; import {R3DeclareDirectiveMetadata, R3DeclareQueryMetadata} from './api'; -import {toOptionalLiteralMap} from './util'; +import {generateForwardRef, toOptionalLiteralMap} from './util'; /** * Every time we make a breaking change to the declaration interface or partial-linker behavior, we @@ -95,8 +95,10 @@ function compileQuery(query: R3QueryMetadata): o.LiteralMapExpr { if (query.first) { meta.set('first', o.literal(true)); } - meta.set( - 'predicate', Array.isArray(query.predicate) ? asLiteral(query.predicate) : query.predicate); + meta.set('predicate', compileQueryPredicate(query)); + if (query.isForwardRef) { + meta.set('isForwardRef', o.literal(query.isForwardRef)); + } if (!query.emitDistinctChangesOnly) { // `emitDistinctChangesOnly` is special because we expect it to be `true`. // Therefore we explicitly emit the field, and explicitly place it only when it's `false`. @@ -114,6 +116,16 @@ function compileQuery(query: R3QueryMetadata): o.LiteralMapExpr { return meta.toLiteralMap(); } +function compileQueryPredicate(query: R3QueryMetadata): o.Expression { + if (Array.isArray(query.predicate)) { + return asLiteral(query.predicate); + } else if (query.isForwardRef) { + return generateForwardRef(query.predicate); + } else { + return query.predicate; + } +} + /** * Compiles the host metadata into its partial declaration form as declared * in `R3DeclareDirectiveMetadata['host']` diff --git a/packages/compiler/src/render3/view/api.ts b/packages/compiler/src/render3/view/api.ts index 2e1d67a545b0dc..2ac73845ebb425 100644 --- a/packages/compiler/src/render3/view/api.ts +++ b/packages/compiler/src/render3/view/api.ts @@ -304,6 +304,11 @@ export interface R3QueryMetadata { */ predicate: o.Expression|string[]; + /** + * True if the `predicate` is an expression that was wrapped in a `forwardRef()` call. + */ + isForwardRef?: boolean; + /** * Whether to include only direct children or all descendants. */ diff --git a/packages/core/src/compiler/compiler_facade_interface.ts b/packages/core/src/compiler/compiler_facade_interface.ts index 6a27e8e1786e00..6c5b1c6051a1e1 100644 --- a/packages/core/src/compiler/compiler_facade_interface.ts +++ b/packages/core/src/compiler/compiler_facade_interface.ts @@ -278,6 +278,7 @@ export interface R3DeclareQueryMetadataFacade { propertyName: string; first?: boolean; predicate: OpaqueValue|string[]; + isForwardRef?: boolean; descendants?: boolean; read?: OpaqueValue; static?: boolean; diff --git a/packages/core/test/render3/jit/declare_directive_spec.ts b/packages/core/test/render3/jit/declare_directive_spec.ts index 9b85febbcb2aa9..73494176d4d90d 100644 --- a/packages/core/test/render3/jit/declare_directive_spec.ts +++ b/packages/core/test/render3/jit/declare_directive_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ElementRef, ɵɵngDeclareDirective} from '@angular/core'; +import {ElementRef, forwardRef, ɵɵngDeclareDirective} from '@angular/core'; import {AttributeMarker, DirectiveDef} from '../../../src/render3'; import {functionContaining} from './matcher'; @@ -118,6 +118,29 @@ describe('directive declaration jit compilation', () => { }); }); + it('should compile content queries with forwardRefs', () => { + class Child {} + const def = ɵɵngDeclareDirective({ + type: TestClass, + queries: [ + { + propertyName: 'byRef', + predicate: forwardRef(() => Child), + isForwardRef: true, + }, + ], + }) as DirectiveDef; + + + expectDirectiveDef(def, { + contentQueries: functionContaining([ + // NOTE: the `anonymous` match is to support IE11, as functions don't have a name there. + /(?:contentQuery|anonymous)[^(]*\(dirIndex,[^,]*Child[^,]*,4\)/, + '(ctx.byRef = _t)', + ]), + }); + }); + it('should compile view queries', () => { const def = ɵɵngDeclareDirective({ type: TestClass,