Content-Length: 1024341 | pFad | https://github.com/angular/angular/commit/0361c2d81f5d2c56597002f465c00e9b1c4003e4

9F feat(compiler): support void operator in templates (#59894) · angular/angular@0361c2d · GitHub
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)








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: https://github.com/angular/angular/commit/0361c2d81f5d2c56597002f465c00e9b1c4003e4

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy