Skip to content

Commit 51b8ff2

Browse files
committed
feat(compiler): support tagged template literals in expressions (angular#59947)
Adds support for using tagged template literals in Angular templates. Ex: ``` @component({ template: '{{ greet`Hello, ${name()}` }}' }) export class MyComp { name = input(); greet(strings: TemplateStringsArray, name: string) { return strings[0] + name + strings[1] + '!'; } } ``` PR Close angular#59947
1 parent f9043e2 commit 51b8ff2

File tree

20 files changed

+547
-88
lines changed

20 files changed

+547
-88
lines changed

adev/src/content/guide/templates/expression-syntax.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@ Angular supports a subset of [literal values](https://developer.mozilla.org/en-U
88

99
### Supported value literals
1010

11-
| Literal type | Example values |
12-
| --------------- | ------------------------------- |
13-
| String | `'Hello'`, `"World"` |
14-
| Boolean | `true`, `false` |
15-
| Number | `123`, `3.14` |
16-
| Object | `{name: 'Alice'}` |
17-
| Array | `['Onion', 'Cheese', 'Garlic']` |
18-
| null | `null` |
19-
| Template string | `` `Hello ${name}` `` |
11+
| Literal type | Example values |
12+
| ---------------------- | ------------------------------- |
13+
| String | `'Hello'`, `"World"` |
14+
| Boolean | `true`, `false` |
15+
| Number | `123`, `3.14` |
16+
| Object | `{name: 'Alice'}` |
17+
| Array | `['Onion', 'Cheese', 'Garlic']` |
18+
| null | `null` |
19+
| Template string | `` `Hello ${name}` `` |
20+
| Tagged template string | `` tag`Hello ${name}` `` |
2021

2122
### Unsupported literals
2223

packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
SafeCall,
3131
SafeKeyedRead,
3232
SafePropertyRead,
33+
TaggedTemplateLiteral,
3334
TemplateLiteral,
3435
TemplateLiteralElement,
3536
ThisReceiver,
@@ -38,12 +39,9 @@ import {
3839
VoidExpression,
3940
} from '@angular/compiler';
4041
import ts from 'typescript';
41-
4242
import {TypeCheckingConfig} from '../api';
43-
4443
import {addParseSpanInfo, wrapForDiagnostics, wrapForTypeChecker} from './diagnostics';
4544
import {tsCastToAny, tsNumericExpression} from './ts_util';
46-
4745
/**
4846
* Expression that is cast to any. Currently represented as `0 as any`.
4947
*
@@ -484,6 +482,14 @@ class AstTranslator implements AstVisitor {
484482
throw new Error('Method not implemented');
485483
}
486484

485+
visitTaggedTemplateLiteral(ast: TaggedTemplateLiteral): ts.TaggedTemplateExpression {
486+
return ts.factory.createTaggedTemplateExpression(
487+
this.translate(ast.tag),
488+
undefined,
489+
this.visitTemplateLiteral(ast.template),
490+
);
491+
}
492+
487493
private convertToSafeCall(
488494
ast: Call | SafeCall,
489495
expr: ts.Expression,
@@ -615,4 +621,7 @@ class VeSafeLhsInferenceBugDetector implements AstVisitor {
615621
visitTemplateLiteralElement(ast: TemplateLiteralElement, context: any) {
616622
return false;
617623
}
624+
visitTaggedTemplateLiteral(ast: TaggedTemplateLiteral, context: any) {
625+
return false;
626+
}
618627
}

packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,16 @@ describe('type check blocks', () => {
166166
);
167167
});
168168

169+
it('should handle tagged template literals', () => {
170+
expect(tcb('{{ tag`hello world` }}')).toContain('"" + (((this).tag) `hello world`);');
171+
expect(tcb('{{ tag`hello \\${name}!!!` }}')).toContain(
172+
'"" + (((this).tag) `hello \\${name}!!!`);',
173+
);
174+
expect(tcb('{{ tag`${a} - ${b} - ${c}` }}')).toContain(
175+
'"" + (((this).tag) `${((this).a)} - ${((this).b)} - ${((this).c)}`);',
176+
);
177+
});
178+
169179
describe('type constructors', () => {
170180
it('should handle missing property bindings', () => {
171181
const TEMPLATE = `<div dir [inputA]="foo"></div>`;

packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/value_composition/GOLDEN_PARTIAL.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,3 +780,62 @@ export declare class MyApp {
780780
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "my-app", never, {}, {}, never, never, true, never>;
781781
}
782782

783+
/****************************************************************************************************
784+
* PARTIAL FILE: tagged_template_literals.js
785+
****************************************************************************************************/
786+
import { Component, Pipe } from '@angular/core';
787+
import * as i0 from "@angular/core";
788+
export class UppercasePipe {
789+
transform(value) {
790+
return value.toUpperCase();
791+
}
792+
}
793+
UppercasePipe.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: UppercasePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
794+
UppercasePipe.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: UppercasePipe, isStandalone: true, name: "uppercase" });
795+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: UppercasePipe, decorators: [{
796+
type: Pipe,
797+
args: [{ name: 'uppercase' }]
798+
}] });
799+
export class MyApp {
800+
constructor() {
801+
this.name = 'Frodo';
802+
this.timeOfDay = 'morning';
803+
this.tag = (strings, ...args) => '';
804+
}
805+
}
806+
MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component });
807+
MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, isStandalone: true, selector: "my-app", ngImport: i0, template: `
808+
<div>No interpolations: {{ tag\`hello world \` }}</div>
809+
<span>With interpolations: {{ tag\`hello \${name}, it is currently \${timeOfDay}!\` }}</span>
810+
<p>With pipe: {{ tag\`hello \${name}\` | uppercase }}</p>
811+
`, isInline: true, dependencies: [{ kind: "pipe", type: UppercasePipe, name: "uppercase" }] });
812+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{
813+
type: Component,
814+
args: [{
815+
selector: 'my-app',
816+
template: `
817+
<div>No interpolations: {{ tag\`hello world \` }}</div>
818+
<span>With interpolations: {{ tag\`hello \${name}, it is currently \${timeOfDay}!\` }}</span>
819+
<p>With pipe: {{ tag\`hello \${name}\` | uppercase }}</p>
820+
`,
821+
imports: [UppercasePipe],
822+
}]
823+
}] });
824+
825+
/****************************************************************************************************
826+
* PARTIAL FILE: tagged_template_literals.d.ts
827+
****************************************************************************************************/
828+
import * as i0 from "@angular/core";
829+
export declare class UppercasePipe {
830+
transform(value: string): string;
831+
static ɵfac: i0.ɵɵFactoryDeclaration<UppercasePipe, never>;
832+
static ɵpipe: i0.ɵɵPipeDeclaration<UppercasePipe, "uppercase", true>;
833+
}
834+
export declare class MyApp {
835+
name: string;
836+
timeOfDay: string;
837+
tag: (strings: TemplateStringsArray, ...args: string[]) => string;
838+
static ɵfac: i0.ɵɵFactoryDeclaration<MyApp, never>;
839+
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "my-app", never, {}, {}, never, never, true, never>;
840+
}
841+

packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/value_composition/TEST_CASES.json

Lines changed: 30 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33
"cases": [
44
{
55
"description": "should instantiate directives",
6-
"inputFiles": [
7-
"directives.ts"
8-
],
6+
"inputFiles": ["directives.ts"],
97
"expectations": [
108
{
119
"failureMessage": "Incorrect ChildComponent.ɵcmp",
@@ -62,16 +60,11 @@
6260
]
6361
}
6462
],
65-
"compilationModeFilter": [
66-
"full compile",
67-
"local compile"
68-
]
63+
"compilationModeFilter": ["full compile", "local compile"]
6964
},
7065
{
7166
"description": "should support complex selectors",
72-
"inputFiles": [
73-
"complex_selectors.ts"
74-
],
67+
"inputFiles": ["complex_selectors.ts"],
7568
"expectations": [
7669
{
7770
"failureMessage": "Incorrect SomeDirective.ɵdir",
@@ -113,23 +106,17 @@
113106
},
114107
{
115108
"description": "should convert #my-app selector to [\"\", \"id\", \"my-app\"]",
116-
"inputFiles": [
117-
"id_selector.ts"
118-
],
109+
"inputFiles": ["id_selector.ts"],
119110
"expectations": [
120111
{
121112
"failureMessage": "Incorrect SomeComponent.ɵcomp",
122-
"files": [
123-
"id_selector.js"
124-
]
113+
"files": ["id_selector.js"]
125114
}
126115
]
127116
},
128117
{
129118
"description": "should support components without selector",
130-
"inputFiles": [
131-
"no_selector.ts"
132-
],
119+
"inputFiles": ["no_selector.ts"],
133120
"expectations": [
134121
{
135122
"failureMessage": "Incorrect EmptyOutletComponent.ɵcmp",
@@ -153,9 +140,7 @@
153140
},
154141
{
155142
"description": "should not treat ElementRef, ViewContainerRef, or ChangeDetectorRef specially when injecting",
156-
"inputFiles": [
157-
"view_tokens_di.ts"
158-
],
143+
"inputFiles": ["view_tokens_di.ts"],
159144
"expectations": [
160145
{
161146
"failureMessage": "Incorrect MyComponent.ɵcmp",
@@ -179,9 +164,7 @@
179164
},
180165
{
181166
"description": "should support structural directives",
182-
"inputFiles": [
183-
"structural_directives.ts"
184-
],
167+
"inputFiles": ["structural_directives.ts"],
185168
"expectations": [
186169
{
187170
"failureMessage": "Incorrect IfDirective.ɵdir",
@@ -223,85 +206,71 @@
223206
},
224207
{
225208
"description": "should support array literals",
226-
"inputFiles": [
227-
"array_literals.ts"
228-
],
209+
"inputFiles": ["array_literals.ts"],
229210
"expectations": [
230211
{
231212
"failureMessage": "Invalid array emit",
232-
"files": [
233-
"array_literals.js"
234-
]
213+
"files": ["array_literals.js"]
235214
}
236215
]
237216
},
238217
{
239218
"description": "should support 9+ bindings in array literals",
240-
"inputFiles": [
241-
"array_literals_many.ts"
242-
],
219+
"inputFiles": ["array_literals_many.ts"],
243220
"expectations": [
244221
{
245222
"failureMessage": "Invalid array binding",
246-
"files": [
247-
"array_literals_many.js"
248-
]
223+
"files": ["array_literals_many.js"]
249224
}
250225
]
251226
},
252227
{
253228
"description": "should support object literals",
254-
"inputFiles": [
255-
"object_literals.ts"
256-
],
229+
"inputFiles": ["object_literals.ts"],
257230
"expectations": [
258231
{
259232
"failureMessage": "Invalid object literal binding",
260-
"files": [
261-
"object_literals.js"
262-
]
233+
"files": ["object_literals.js"]
263234
}
264235
]
265236
},
266237
{
267238
"description": "should support expressions nested deeply in object/array literals",
268-
"inputFiles": [
269-
"literal_nested_expression.ts"
270-
],
239+
"inputFiles": ["literal_nested_expression.ts"],
271240
"expectations": [
272241
{
273242
"failureMessage": "Invalid array/object literal binding",
274-
"files": [
275-
"literal_nested_expression.js"
276-
]
243+
"files": ["literal_nested_expression.js"]
277244
}
278245
]
279246
},
280247
{
281248
"description": "should support number literals with separators",
282-
"inputFiles": [
283-
"number_separator.ts"
284-
],
249+
"inputFiles": ["number_separator.ts"],
285250
"expectations": [
286251
{
287252
"failureMessage": "Invalid number literal",
288-
"files": [
289-
"number_separator.js"
290-
]
253+
"files": ["number_separator.js"]
291254
}
292255
]
293256
},
294257
{
295258
"description": "should support template literals",
296-
"inputFiles": [
297-
"template_literals.ts"
298-
],
259+
"inputFiles": ["template_literals.ts"],
299260
"expectations": [
300261
{
301262
"failureMessage": "Invalid template literal binding",
302-
"files": [
303-
"template_literals.js"
304-
]
263+
"files": ["template_literals.js"]
264+
}
265+
]
266+
},
267+
{
268+
"description": "should support tagged template literals",
269+
"inputFiles": ["tagged_template_literals.ts"],
270+
"expectations": [
271+
{
272+
"failureMessage": "Invalid tagged template literal binding",
273+
"files": ["tagged_template_literals.js"]
305274
}
306275
]
307276
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
if (rf & 2) {
2+
$r3$.ɵɵadvance();
3+
$r3$.ɵɵtextInterpolate1("No interpolations: ", ctx.tag `hello world `, "");
4+
$r3$.ɵɵadvance(2);
5+
$r3$.ɵɵtextInterpolate1("With interpolations: ", ctx.tag `hello ${ctx.name}, it is currently ${ctx.timeOfDay}!`, "");
6+
$r3$.ɵɵadvance(2);
7+
$r3$.ɵɵtextInterpolate1("With pipe: ", $r3$.ɵɵpipeBind1(6, 3, ctx.tag `hello ${ctx.name}`), "");
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import {Component, Pipe} from '@angular/core';
2+
3+
@Pipe({name: 'uppercase'})
4+
export class UppercasePipe {
5+
transform(value: string) {
6+
return value.toUpperCase();
7+
}
8+
}
9+
10+
@Component({
11+
selector: 'my-app',
12+
template: `
13+
<div>No interpolations: {{ tag\`hello world \` }}</div>
14+
<span>With interpolations: {{ tag\`hello \${name}, it is currently \${timeOfDay}!\` }}</span>
15+
<p>With pipe: {{ tag\`hello \${name}\` | uppercase }}</p>
16+
`,
17+
imports: [UppercasePipe],
18+
})
19+
export class MyApp {
20+
name = 'Frodo';
21+
timeOfDay = 'morning';
22+
tag = (strings: TemplateStringsArray, ...args: string[]) => '';
23+
}

0 commit comments

Comments
 (0)