Skip to content

Commit d9923b7

Browse files
crisbetothePunderWoman
authored andcommitted
feat(core): support arrow functions in expressions
Adds support for using arrow functions in Angular expressions. They generally behave like JS arrow functions with the same access as other Angular expressions, but with the following limitations: * We only support arrow functions with implicit returns, e.g. `(a) => a + 1` is allowed while `(a) => { return a + 1 }` is not. * Pipes can't be used inside arrow functions, but they can be passed through to pipes. To avoid recreating the functions in each change detection, the compiler applies a couple of optimizations: * If an arrow function only references its own parameters, it is extracted into a top-level constant that is passed around to the different usage sites. * If an arrow function has references to the template context, we store it on the current view and read the stored value later on. Fixes #14129.
1 parent 3242a61 commit d9923b7

File tree

50 files changed

+2218
-61
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+2218
-61
lines changed

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

Lines changed: 606 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
{
2+
"$schema": "../test_case_schema.json",
3+
"cases": [
4+
{
5+
"description": "should reuse arrow functions that do not depend on context",
6+
"inputFiles": ["arrow_function_no_context.ts"],
7+
"expectations": [
8+
{
9+
"failureMessage": "Invalid template",
10+
"files": ["arrow_function_no_context.js"]
11+
}
12+
]
13+
},
14+
{
15+
"description": "should handle arrow function that only accesses the top-level context",
16+
"inputFiles": ["arrow_function_top_level_context.ts"],
17+
"expectations": [
18+
{
19+
"failureMessage": "Invalid template",
20+
"files": ["arrow_function_top_level_context.js"]
21+
}
22+
]
23+
},
24+
{
25+
"description": "should handle arrow function that has a `this` access with same name as parameter",
26+
"inputFiles": ["arrow_function_this_access.ts"],
27+
"expectations": [
28+
{
29+
"failureMessage": "Invalid template",
30+
"files": ["arrow_function_this_access.js"]
31+
}
32+
]
33+
},
34+
{
35+
"description": "should handle arrow function that accesses @let declarations across view boundaries",
36+
"inputFiles": ["arrow_function_let_nested.ts"],
37+
"expectations": [
38+
{
39+
"failureMessage": "Invalid template",
40+
"files": ["arrow_function_let_nested.js"]
41+
}
42+
]
43+
},
44+
{
45+
"description": "should handle arrow function that access context in parent views, inside event listeners",
46+
"inputFiles": ["arrow_function_nested_listeners.ts"],
47+
"expectations": [
48+
{
49+
"failureMessage": "Invalid template",
50+
"files": ["arrow_function_nested_listeners.js"]
51+
}
52+
]
53+
},
54+
{
55+
"description": "should handle arrow function that is defined inside a @let and used within the template",
56+
"inputFiles": ["arrow_function_defined_let.ts"],
57+
"expectations": [
58+
{
59+
"failureMessage": "Invalid template",
60+
"files": ["arrow_function_defined_let.js"]
61+
}
62+
]
63+
},
64+
{
65+
"description": "should handle arrow function inside a host binding",
66+
"inputFiles": ["arrow_function_host_binding.ts"],
67+
"expectations": [
68+
{
69+
"failureMessage": "Invalid template",
70+
"files": ["arrow_function_host_binding.js"]
71+
}
72+
]
73+
},
74+
{
75+
"description": "should handle arrow function inside a host listener",
76+
"inputFiles": ["arrow_function_host_listener.ts"],
77+
"expectations": [
78+
{
79+
"failureMessage": "Invalid template",
80+
"files": ["arrow_function_host_listener.js"]
81+
}
82+
]
83+
},
84+
{
85+
"description": "should not produce pure functions for arrow function return values",
86+
"inputFiles": ["arrow_function_pure_return_values.ts"],
87+
"expectations": [
88+
{
89+
"failureMessage": "Invalid template",
90+
"files": ["arrow_function_pure_return_values.js"]
91+
}
92+
]
93+
},
94+
{
95+
"description": "should be able to use arrow functions inside pure values",
96+
"inputFiles": ["arrow_function_inside_pure_value.ts"],
97+
"expectations": [
98+
{
99+
"failureMessage": "Invalid template",
100+
"files": ["arrow_function_inside_pure_value.js"]
101+
}
102+
]
103+
},
104+
{
105+
"description": "should handle arrow function returning another arrow function with no context access",
106+
"inputFiles": ["arrow_function_returning_arrow_function_no_context.ts"],
107+
"expectations": [
108+
{
109+
"failureMessage": "Invalid template",
110+
"files": ["arrow_function_returning_arrow_function_no_context.js"]
111+
}
112+
]
113+
},
114+
{
115+
"description": "should handle arrow function returning another arrow function with only top-level context access",
116+
"inputFiles": ["arrow_function_returning_arrow_function_top_level_context.ts"],
117+
"expectations": [
118+
{
119+
"failureMessage": "Invalid template",
120+
"files": ["arrow_function_returning_arrow_function_top_level_context.js"]
121+
}
122+
]
123+
},
124+
{
125+
"description": "should handle arrow function returning another arrow function with access across multiple contexts",
126+
"inputFiles": ["arrow_function_returning_arrow_function_nested_context.ts"],
127+
"expectations": [
128+
{
129+
"failureMessage": "Invalid template",
130+
"files": ["arrow_function_returning_arrow_function_nested_context.js"]
131+
}
132+
]
133+
},
134+
{
135+
"description": "should handle arrow function with optional reads",
136+
"inputFiles": ["arrow_function_safe_access.ts"],
137+
"expectations": [
138+
{
139+
"failureMessage": "Invalid template",
140+
"files": ["arrow_function_safe_access.js"]
141+
}
142+
]
143+
},
144+
{
145+
"description": "should handle arrow function with optional reads in nested views",
146+
"inputFiles": ["arrow_function_safe_access_nested_views.ts"],
147+
"expectations": [
148+
{
149+
"failureMessage": "Invalid template",
150+
"files": ["arrow_function_safe_access_nested_views.js"]
151+
}
152+
]
153+
},
154+
{
155+
"description": "should handle arrow function that is passed into a pipe",
156+
"inputFiles": ["arrow_function_pipe.ts"],
157+
"expectations": [
158+
{
159+
"failureMessage": "Invalid template",
160+
"files": ["arrow_function_pipe.js"]
161+
}
162+
]
163+
}
164+
]
165+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
function TestComp_Conditional_2_Template(rf, ctx) {
2+
if (rf & 1) {
3+
const $_r2$ = $r3$.ɵɵgetCurrentView();
4+
$r3$.ɵɵtext(0);
5+
$r3$.ɵɵdomElementStart(1, "button", 0);
6+
$r3$.ɵɵdomListener("click", function TestComp_Conditional_2_Template_button_click_1_listener() {
7+
$r3$.ɵɵrestoreView($_r2$);
8+
const $ctx_r2$ = $r3$.ɵɵnextContext();
9+
const $fn_r4$ = $r3$.ɵɵreadContextLet(0);
10+
return $r3$.ɵɵresetView($ctx_r2$.componentValue = $fn_r4$(2, 1));
11+
});
12+
$r3$.ɵɵdomElementEnd();
13+
}
14+
if (rf & 2) {
15+
$r3$.ɵɵnextContext();
16+
const $fn_r4$ = $r3$.ɵɵreadContextLet(0);
17+
$r3$.ɵɵtextInterpolate1(" Two: ", $fn_r4$(1, 1), " ");
18+
}
19+
}
20+
21+
$r3$.ɵɵdefineComponent({
22+
23+
decls: 4,
24+
vars: 3,
25+
consts: [[3, "click"]],
26+
template: function TestComp_Template(rf, ctx) {
27+
if (rf & 1) {
28+
const $_r1$ = $r3$.ɵɵgetCurrentView();
29+
$r3$.ɵɵdeclareLet(0);
30+
$r3$.ɵɵtext(1);
31+
$r3$.ɵɵconditionalCreate(2, TestComp_Conditional_2_Template, 2, 1);
32+
// NOTE: the restoreView and resetView calls here are the result of variable optimization not picking up some cases. We can remove them once #66286 is resolved.
33+
$r3$.ɵɵstoreCallback(3, (a, b) => {
34+
$r3$.ɵɵrestoreView($_r1$);
35+
return $r3$.ɵɵresetView(ctx.componentValue + a + b);
36+
});
37+
}
38+
if (rf & 2) {
39+
const $_callbackFn0_r5$ = $r3$.ɵɵgetCallback(3);
40+
const $fn_r6$ = $r3$.ɵɵstoreLet($_callbackFn0_r5$);
41+
$r3$.ɵɵadvance();
42+
$r3$.ɵɵtextInterpolate1(" One: ", $fn_r6$(0, 1), " ");
43+
$r3$.ɵɵadvance();
44+
$r3$.ɵɵconditional(true ? 2 : -1);
45+
}
46+
},
47+
48+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {Component} from '@angular/core';
2+
3+
@Component({
4+
template: `
5+
@let fn = (a, b) => componentValue + a + b;
6+
One: {{fn(0, 1)}}
7+
8+
@if (true) {
9+
Two: {{fn(1, 1)}}
10+
11+
<button (click)="componentValue = fn(2, 1)"></button>
12+
}
13+
`
14+
})
15+
export class TestComp {
16+
componentValue = 0;
17+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const $_callbackFn0$ = (a, b) => a / b;
2+
3+
$r3$.ɵɵdefineDirective({
4+
5+
hostVars: 2,
6+
hostBindings: function TestDir_HostBindings(rf, ctx) {
7+
if (rf & 2) {
8+
$r3$.ɵɵattribute("no-context", $_callbackFn0$(5, 10))(
9+
// NOTE: the inline declaration with context access isn't optimized due to #66263.
10+
"with-context", ((a, b) => a / b + ctx.componentProp)(6, 12));
11+
}
12+
}
13+
14+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {Directive} from '@angular/core';
2+
3+
@Directive({
4+
host: {
5+
'[attr.no-context]': '((a, b) => a / b)(5, 10)',
6+
'[attr.with-context]': '((a, b) => a / b + componentProp)(6, 12)',
7+
}
8+
})
9+
export class TestDir {
10+
componentProp = 1;
11+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const $_callbackFn0$ = prev => prev + 1;
2+
3+
$r3$.ɵɵdefineDirective({
4+
5+
hostBindings: function TestDir_HostBindings(rf, ctx) {
6+
if (rf & 1) {
7+
$r3$.ɵɵlistener("click", function TestDir_click_HostBindingHandler() {
8+
return ctx.someSignal.update($_callbackFn0$);
9+
})("mousedown", function TestDir_mousedown_HostBindingHandler() {
10+
// NOTE: the inline declaration with context access isn't optimized due to #66263.
11+
return ctx.someSignal.update(() => ctx.componentProp + 1);
12+
});
13+
}
14+
}
15+
16+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import {Directive, signal} from '@angular/core';
2+
3+
@Directive({
4+
host: {
5+
'(click)': 'someSignal.update(prev => prev + 1)',
6+
'(mousedown)': 'someSignal.update(() => componentProp + 1)',
7+
}
8+
})
9+
export class TestDir {
10+
someSignal = signal(0);
11+
componentProp = 1;
12+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const $_c0$ = a0 => [a0];
2+
const $_callbackFn0$ = a => a + 1;
3+
4+
$r3$.ɵɵdefineComponent({
5+
6+
decls: 2,
7+
vars: 6,
8+
template: function TestComp_Template(rf, ctx) {
9+
if (rf & 1) {
10+
$r3$.ɵɵtext(0);
11+
$r3$.ɵɵstoreCallback(1, a => a + 1 + ctx.componentProp);
12+
}
13+
if (rf & 2) {
14+
const $_callbackFn1_r1$ = $r3$.ɵɵgetCallback(1);
15+
$r3$.ɵɵtextInterpolate2(" ", $r3$.ɵɵpureFunction1(2, $_c0$, $_callbackFn0$)[0](1000), " ",
16+
$r3$.ɵɵpureFunction1(4, $_c0$, $_callbackFn1_r1$)[0](1000), " ");
17+
}
18+
},
19+
20+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {Component} from '@angular/core';
2+
3+
@Component({
4+
template: `
5+
{{[(a) => a + 1][0](1000)}}
6+
{{[(a) => a + 1 + componentProp][0](1000)}}
7+
`
8+
})
9+
export class TestComp {
10+
componentProp = 0;
11+
}

0 commit comments

Comments
 (0)