diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 67722e6589..58b4110543 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -886,8 +886,8 @@ export class ComponentDecoratorHandler implements content: analysis.template.content, sourceUrl: analysis.template.declaration.resolvedTemplateUrl, isInline: analysis.template.declaration.isInline, - inlineTemplateExpression: analysis.template.declaration.isInline ? - new WrappedNodeExpr(analysis.template.declaration.expression) : + inlineTemplateLiteralExpression: analysis.template.sourceMapping.type === 'direct' ? + new WrappedNodeExpr(analysis.template.sourceMapping.node) : null, }; const meta: R3ComponentMetadata = {...analysis.meta, ...resolution}; diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/GOLDEN_PARTIAL.js index 3a6066e816..c4d6e1f421 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/GOLDEN_PARTIAL.js @@ -493,3 +493,77 @@ export declare class MyModule { static ɵinj: i0.ɵɵInjectorDeclaration; } +/**************************************************************************************************** + * PARTIAL FILE: non_literal_template.js + ****************************************************************************************************/ +import { Component } from '@angular/core'; +import * as i0 from "@angular/core"; +const myTemplate = `
Hello
`; +export class TestCmp { +} +TestCmp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestCmp, deps: [], target: i0.ɵɵFactoryTarget.Component }); +TestCmp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", type: TestCmp, selector: "test-cmp", ngImport: i0, template: "
Hello
", isInline: true }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestCmp, decorators: [{ + type: Component, + args: [{ selector: 'test-cmp', template: myTemplate }] + }] }); + +/**************************************************************************************************** + * PARTIAL FILE: non_literal_template.d.ts + ****************************************************************************************************/ +import * as i0 from "@angular/core"; +export declare class TestCmp { + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} + +/**************************************************************************************************** + * PARTIAL FILE: non_literal_template_with_substitution.js + ****************************************************************************************************/ +import { Component } from '@angular/core'; +import * as i0 from "@angular/core"; +const greeting = 'Hello!'; +const myTemplate = `
${greeting}
`; +export class TestCmp { +} +TestCmp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestCmp, deps: [], target: i0.ɵɵFactoryTarget.Component }); +TestCmp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", type: TestCmp, selector: "test-cmp", ngImport: i0, template: "
Hello!
", isInline: true }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestCmp, decorators: [{ + type: Component, + args: [{ selector: 'test-cmp', template: myTemplate }] + }] }); + +/**************************************************************************************************** + * PARTIAL FILE: non_literal_template_with_substitution.d.ts + ****************************************************************************************************/ +import * as i0 from "@angular/core"; +export declare class TestCmp { + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} + +/**************************************************************************************************** + * PARTIAL FILE: non_literal_template_with_concatenation.js + ****************************************************************************************************/ +import { Component } from '@angular/core'; +import * as i0 from "@angular/core"; +const greeting = 'Hello!'; +const myTemplate = '
' + greeting + '
'; +export class TestCmp { +} +TestCmp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestCmp, deps: [], target: i0.ɵɵFactoryTarget.Component }); +TestCmp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", type: TestCmp, selector: "test-cmp", ngImport: i0, template: "
Hello!
", isInline: true }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestCmp, decorators: [{ + type: Component, + args: [{ selector: 'test-cmp', template: myTemplate }] + }] }); + +/**************************************************************************************************** + * PARTIAL FILE: non_literal_template_with_concatenation.d.ts + ****************************************************************************************************/ +import * as i0 from "@angular/core"; +export declare class TestCmp { + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} + diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/TEST_CASES.json index e21aaad044..0e4f3454d7 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/TEST_CASES.json @@ -160,6 +160,24 @@ ] } ] + }, + { + "description": "should support inline non-literal templates", + "inputFiles": [ + "non_literal_template.ts" + ] + }, + { + "description": "should support inline non-literal templates using substitution", + "inputFiles": [ + "non_literal_template_with_substitution.ts" + ] + }, + { + "description": "should support inline non-literal templates using string concatenation", + "inputFiles": [ + "non_literal_template_with_concatenation.ts" + ] } ] } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/non_literal_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/non_literal_template.js new file mode 100644 index 0000000000..57cc135d9f --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/non_literal_template.js @@ -0,0 +1,7 @@ +function TestCmp_div_0_Template(rf, ctx) { + if (rf & 1) { + i0.ɵɵelementStart(0, "div"); + i0.ɵɵtext(1, "Hello"); + i0.ɵɵelementEnd(); + } +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/non_literal_template.ts b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/non_literal_template.ts new file mode 100644 index 0000000000..57334f6368 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/non_literal_template.ts @@ -0,0 +1,7 @@ +import {Component} from '@angular/core'; + +const myTemplate = `
Hello
`; + +@Component({selector: 'test-cmp', template: myTemplate}) +export class TestCmp { +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/non_literal_template_with_concatenation.js b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/non_literal_template_with_concatenation.js new file mode 100644 index 0000000000..196bbb7f13 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/non_literal_template_with_concatenation.js @@ -0,0 +1,7 @@ +function TestCmp_div_0_Template(rf, ctx) { + if (rf & 1) { + i0.ɵɵelementStart(0, "div"); + i0.ɵɵtext(1, "Hello!"); + i0.ɵɵelementEnd(); + } +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/non_literal_template_with_concatenation.ts b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/non_literal_template_with_concatenation.ts new file mode 100644 index 0000000000..8ab665d5cf --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/non_literal_template_with_concatenation.ts @@ -0,0 +1,8 @@ +import {Component} from '@angular/core'; + +const greeting = 'Hello!'; +const myTemplate = '
' + greeting + '
'; + +@Component({selector: 'test-cmp', template: myTemplate}) +export class TestCmp { +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/non_literal_template_with_substitution.js b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/non_literal_template_with_substitution.js new file mode 100644 index 0000000000..196bbb7f13 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/non_literal_template_with_substitution.js @@ -0,0 +1,7 @@ +function TestCmp_div_0_Template(rf, ctx) { + if (rf & 1) { + i0.ɵɵelementStart(0, "div"); + i0.ɵɵtext(1, "Hello!"); + i0.ɵɵelementEnd(); + } +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/non_literal_template_with_substitution.ts b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/non_literal_template_with_substitution.ts new file mode 100644 index 0000000000..39095dc13c --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/non_literal_template_with_substitution.ts @@ -0,0 +1,8 @@ +import {Component} from '@angular/core'; + +const greeting = 'Hello!'; +const myTemplate = `
${greeting}
`; + +@Component({selector: 'test-cmp', template: myTemplate}) +export class TestCmp { +} diff --git a/packages/compiler/src/render3/partial/component.ts b/packages/compiler/src/render3/partial/component.ts index 5f56a06384..fb620b55bd 100644 --- a/packages/compiler/src/render3/partial/component.ts +++ b/packages/compiler/src/render3/partial/component.ts @@ -42,8 +42,11 @@ export interface DeclareComponentTemplateInfo { */ isInline: boolean; - /** Expression that resolves to the inline template. */ - inlineTemplateExpression: o.Expression|null; + /** + * If the template was defined inline by a direct string literal, then this is that literal + * expression. Otherwise `null`, if the template was not defined inline or was not a literal. + */ + inlineTemplateLiteralExpression: o.Expression|null; } /** @@ -111,19 +114,30 @@ export function createComponentDefinitionMap( function getTemplateExpression( template: ParsedTemplate, templateInfo: DeclareComponentTemplateInfo): o.Expression { - if (templateInfo.isInline) { - // The template is inline so we can just reuse the current expression node. - return templateInfo.inlineTemplateExpression!; - } else { - // The template is external so we must synthesize an expression node with the appropriate - // source-span. - const contents = templateInfo.content; - const file = new ParseSourceFile(contents, templateInfo.sourceUrl); - const start = new ParseLocation(file, 0, 0, 0); - const end = computeEndLocation(file, contents); - const span = new ParseSourceSpan(start, end); - return o.literal(contents, null, span); + // If the template has been defined using a direct literal, we use that expression directly + // without any modifications. This is ensures proper source mapping from the partially + // compiled code to the source file declaring the template. Note that this does not capture + // template literals referenced indirectly through an identifier. + if (templateInfo.inlineTemplateLiteralExpression !== null) { + return templateInfo.inlineTemplateLiteralExpression; } + + // If the template is defined inline but not through a literal, the template has been resolved + // through static interpretation. We create a literal but cannot provide any source span. Note + // that we cannot use the expression defining the template because the linker expects the template + // to be defined as a literal in the declaration. + if (templateInfo.isInline) { + return o.literal(templateInfo.content, null, null); + } + + // The template is external so we must synthesize an expression node with + // the appropriate source-span. + const contents = templateInfo.content; + const file = new ParseSourceFile(contents, templateInfo.sourceUrl); + const start = new ParseLocation(file, 0, 0, 0); + const end = computeEndLocation(file, contents); + const span = new ParseSourceSpan(start, end); + return o.literal(contents, null, span); } function computeEndLocation(file: ParseSourceFile, contents: string): ParseLocation {