Skip to content

Commit 0361c2d

Browse files
mmalerbakirjs
authored andcommitted
feat(compiler): support void operator in templates (#59894)
Add support for the `void` operator in templates and host bindings. This is useful when binding a listener that may return `false` and unintentionally prevent the default event behavior. Ex: ``` @directive({ host: { '(mousedown)': 'void handleMousedown()' } }) ``` BREAKING CHANGE: `void` in an expression now refers to the operator Previously an expression in the template like `{{void}}` referred to a property on the component class. After this change it now refers to the `void` operator, which would make the above example invalid. If you have existing expressions that need to refer to a property named `void`, change the expression to use `this.void` instead: `{{this.void}}`. PR Close #59894
1 parent 334d851 commit 0361c2d

File tree

31 files changed

+374
-142
lines changed

31 files changed

+374
-142
lines changed

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

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,22 @@ 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}` `` |
2020

2121
### Unsupported literals
2222

23-
| Literal type | Example value |
24-
| ------------------------ | ------------------------- |
25-
| RegExp | `/\d+/` |
26-
| Tagged template string | `` tag`Hello ${name}` `` |
23+
| Literal type | Example value |
24+
| ---------------------- | ------------------------ |
25+
| RegExp | `/\d+/` |
26+
| Tagged template string | `` tag`Hello ${name}` `` |
2727

2828
## Globals
2929

@@ -58,11 +58,13 @@ Angular supports the following operators from standard JavaScript.
5858
| And (Logical) | `&&` |
5959
| Or (Logical) | `\|\|` |
6060
| Not (Logical) | `!` |
61-
| Nullish Coalescing | `const foo = null ?? 'default'` |
61+
| Nullish Coalescing | `possiblyNullValue ?? 'default'` |
6262
| Comparison Operators | `<`, `<=`, `>`, `>=`, `==`, `===`, `!==` |
63-
| Unary Negation | `const y = -x` |
64-
| Unary Plus | `const x = +y` |
65-
| Property Accessor | `person['name'] = 'Mirabel'` |
63+
| Unary Negation | `-x` |
64+
| Unary Plus | `+y` |
65+
| Property Accessor | `person['name']` |
66+
| typeof | `typeof 42` |
67+
| void | `void 1` |
6668

6769
Angular expressions additionally also support the following non-standard operators:
6870

@@ -83,8 +85,6 @@ Angular expressions additionally also support the following non-standard operato
8385
| Object destructuring | `const { name } = person` |
8486
| Array destructuring | `const [firstItem] = items` |
8587
| Comma operator | `x = (x++, x)` |
86-
| typeof | `typeof 42` |
87-
| void | `void 1` |
8888
| in | `'model' in car` |
8989
| instanceof | `car instanceof Automobile` |
9090
| new | `new Car()` |

packages/compiler-cli/linker/babel/src/ast/babel_ast_factory.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@ export class BabelAstFactory implements AstFactory<t.Statement, t.Expression> {
184184
return t.unaryExpression('typeof', expression);
185185
}
186186

187+
createVoidExpression(expression: t.Expression): t.Expression {
188+
return t.unaryExpression('void', expression);
189+
}
190+
187191
createUnaryExpression = t.unaryExpression;
188192

189193
createVariableDeclaration(

packages/compiler-cli/linker/babel/test/ast/babel_ast_factory_spec.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88
import {leadingComment} from '@angular/compiler';
9-
import {template, types as t} from '@babel/core';
9+
import {types as t, template} from '@babel/core';
1010
import _generate from '@babel/generator';
1111

1212
import {BabelAstFactory} from '../../src/ast/babel_ast_factory';
@@ -348,6 +348,14 @@ describe('BabelAstFactory', () => {
348348
});
349349
});
350350

351+
describe('createVoidExpression()', () => {
352+
it('should create a void expression node', () => {
353+
const expr = expression.ast`42`;
354+
const voidExpr = factory.createVoidExpression(expr);
355+
expect(generate(voidExpr).code).toEqual('void 42');
356+
});
357+
});
358+
351359
describe('createUnaryExpression()', () => {
352360
it('should create a unary expression with the operator and operand', () => {
353361
const expr = expression.ast`value`;

packages/compiler-cli/src/ngtsc/translator/src/api/ast_factory.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,13 @@ export interface AstFactory<TStatement, TExpression> {
243243
*/
244244
createTypeOfExpression(expression: TExpression): TExpression;
245245

246+
/**
247+
* Create an expression that evaluates an expression and returns `undefined`.
248+
*
249+
* @param expression the expression whose type we want.
250+
*/
251+
createVoidExpression(expression: TExpression): TExpression;
252+
246253
/**
247254
* Prefix the `operand` with the given `operator` (e.g. `-expr`).
248255
*

packages/compiler-cli/src/ngtsc/translator/src/translator.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,10 @@ export class ExpressionTranslatorVisitor<TFile, TStatement, TExpression>
447447
return this.factory.createTypeOfExpression(ast.expr.visitExpression(this, context));
448448
}
449449

450+
visitVoidExpr(ast: o.VoidExpr, context: Context): TExpression {
451+
return this.factory.createVoidExpression(ast.expr.visitExpression(this, context));
452+
}
453+
450454
visitUnaryOperatorExpr(ast: o.UnaryOperatorExpr, context: Context): TExpression {
451455
if (!UNARY_OPERATORS.has(ast.operator)) {
452456
throw new Error(`Unknown unary operator: ${o.UnaryOperator[ast.operator]}`);

packages/compiler-cli/src/ngtsc/translator/src/type_translator.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,10 @@ class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor {
276276
return ts.factory.createTypeQueryNode(typeNode.typeName);
277277
}
278278

279+
visitVoidExpr(ast: o.VoidExpr, context: Context) {
280+
throw new Error('Method not implemented.');
281+
}
282+
279283
private translateType(type: o.Type, context: Context): ts.TypeNode {
280284
const typeNode = type.visitType(this, context);
281285
if (!ts.isTypeNode(typeNode)) {

packages/compiler-cli/src/ngtsc/translator/src/typescript_ast_factory.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,8 @@ export class TypeScriptAstFactory implements AstFactory<ts.Statement, ts.Express
291291

292292
createTypeOfExpression = ts.factory.createTypeOfExpression;
293293

294+
createVoidExpression = ts.factory.createVoidExpression;
295+
294296
createUnaryExpression(operator: UnaryOperator, operand: ts.Expression): ts.Expression {
295297
return ts.factory.createPrefixUnaryExpression(UNARY_OPERATORS[operator], operand);
296298
}

packages/compiler-cli/src/ngtsc/translator/test/typescript_ast_factory_spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,17 @@ describe('TypeScriptAstFactory', () => {
409409
});
410410
});
411411

412+
describe('createVoidExpression()', () => {
413+
it('should create a void expression node', () => {
414+
const {
415+
items: [expr],
416+
generate,
417+
} = setupExpressions(`42`);
418+
const voidExpr = factory.createVoidExpression(expr);
419+
expect(generate(voidExpr)).toEqual('void 42');
420+
});
421+
});
422+
412423
describe('createUnaryExpression()', () => {
413424
it('should create a unary expression with the operator and operand', () => {
414425
const {

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,17 @@ import {
2525
LiteralPrimitive,
2626
NonNullAssert,
2727
PrefixNot,
28-
TypeofExpression,
2928
PropertyRead,
3029
PropertyWrite,
3130
SafeCall,
3231
SafeKeyedRead,
3332
SafePropertyRead,
34-
ThisReceiver,
35-
Unary,
3633
TemplateLiteral,
3734
TemplateLiteralElement,
35+
ThisReceiver,
36+
TypeofExpression,
37+
Unary,
38+
VoidExpression,
3839
} from '@angular/compiler';
3940
import ts from 'typescript';
4041

@@ -285,6 +286,13 @@ class AstTranslator implements AstVisitor {
285286
return node;
286287
}
287288

289+
visitVoidExpression(ast: VoidExpression): ts.Expression {
290+
const expression = wrapForDiagnostics(this.translate(ast.expression));
291+
const node = ts.factory.createVoidExpression(expression);
292+
addParseSpanInfo(node, ast.sourceSpan);
293+
return node;
294+
}
295+
288296
visitPropertyRead(ast: PropertyRead): ts.Expression {
289297
// This is a normal property read - convert the receiver to an expression and emit the correct
290298
// TypeScript expression to read the property.
@@ -579,10 +587,13 @@ class VeSafeLhsInferenceBugDetector implements AstVisitor {
579587
visitPrefixNot(ast: PrefixNot): boolean {
580588
return ast.expression.visit(this);
581589
}
582-
visitTypeofExpression(ast: PrefixNot): boolean {
590+
visitTypeofExpression(ast: TypeofExpression): boolean {
591+
return ast.expression.visit(this);
592+
}
593+
visitVoidExpression(ast: VoidExpression): boolean {
583594
return ast.expression.visit(this);
584595
}
585-
visitNonNullAssert(ast: PrefixNot): boolean {
596+
visitNonNullAssert(ast: NonNullAssert): boolean {
586597
return ast.expression.visit(this);
587598
}
588599
visitPropertyRead(ast: PropertyRead): boolean {

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ describe('type check blocks', () => {
6969
);
7070
});
7171

72+
it('should handle void expressions', () => {
73+
expect(tcb('{{void a}}')).toContain('void (((this).a))');
74+
expect(tcb('{{!(void a)}}')).toContain('!(void (((this).a)))');
75+
expect(tcb('{{!(void a === "object")}}')).toContain('!((void (((this).a))) === ("object"))');
76+
});
77+
7278
it('should handle attribute values for directive inputs', () => {
7379
const TEMPLATE = `<div dir inputA="value"></div>`;
7480
const DIRECTIVES: TestDeclaration[] = [

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/GOLDEN_PARTIAL.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-
9797
{{ !(typeof {} === 'object') }}
9898
{{ typeof foo?.bar === 'string' }}
9999
{{ typeof foo?.bar | identity }}
100+
{{ void 'test' }}
100101
`, isInline: true, dependencies: [{ kind: "pipe", type: IdentityPipe, name: "identity" }] });
101102
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{
102103
type: Component,
@@ -109,6 +110,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE
109110
{{ !(typeof {} === 'object') }}
110111
{{ typeof foo?.bar === 'string' }}
111112
{{ typeof foo?.bar | identity }}
113+
{{ void 'test' }}
112114
`,
113115
imports: [IdentityPipe],
114116
}]

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/operators.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export class IdentityPipe {
1616
{{ !(typeof {} === 'object') }}
1717
{{ typeof foo?.bar === 'string' }}
1818
{{ typeof foo?.bar | identity }}
19+
{{ void 'test' }}
1920
`,
2021
imports: [IdentityPipe],
2122
})

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/operators_template.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ template: function MyApp_Template(rf, $ctx$) {
33
$i0$.ɵɵtext(0);
44
i0.ɵɵpipe(1, "identity");
55
} if (rf & 2) {
6-
i0.ɵɵtextInterpolate7(" ",
6+
i0.ɵɵtextInterpolate8(" ",
77
1 + 2, " ",
88
1 % 2 + 3 / 4 * 5, " ",
99
+1, " ",
10-
typeof i0.ɵɵpureFunction0(9, _c0) === "object", " ",
11-
!(typeof i0.ɵɵpureFunction0(10, _c0) === "object"), " ",
10+
typeof i0.ɵɵpureFunction0(10, _c0) === "object", " ",
11+
!(typeof i0.ɵɵpureFunction0(11, _c0) === "object"), " ",
1212
typeof (ctx.foo == null ? null : ctx.foo.bar) === "string", " ",
13-
i0.ɵɵpipeBind1(1, 7, typeof (ctx.foo == null ? null : ctx.foo.bar)), " "
13+
i0.ɵɵpipeBind1(1, 8, typeof (ctx.foo == null ? null : ctx.foo.bar)), " ",
14+
void "test", " "
1415
);
1516
}
1617
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy