Skip to content

Commit 8c2698d

Browse files
Merge pull request #21339 from emberjs/nvp/eq-neq-as-keyword
RFC#560 - {{eq}}, {{neq}} as keywords
2 parents 2ff1260 + 761b887 commit 8c2698d

12 files changed

Lines changed: 500 additions & 1 deletion

File tree

packages/@ember/helper/index.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import {
88
hash as glimmerHash,
99
array as glimmerArray,
1010
concat as glimmerConcat,
11+
eq as glimmerEq,
1112
get as glimmerGet,
1213
fn as glimmerFn,
14+
neq as glimmerNeq,
1315
} from '@glimmer/runtime';
1416
import { element as glimmerElement, uniqueId as glimmerUniqueId } from '@ember/-internals/glimmer';
1517
import { type Opaque } from '@ember/-internals/utility-types';
@@ -511,4 +513,52 @@ export interface ElementHelper extends Opaque<'helper:element'> {}
511513
export const uniqueId = glimmerUniqueId;
512514
export type UniqueIdHelper = typeof uniqueId;
513515

516+
/**
517+
* The `{{eq}}` helper returns `true` if its two arguments are strictly equal
518+
* (`===`). Takes exactly two arguments.
519+
*
520+
* ```js
521+
* import { eq } from '@ember/helper';
522+
*
523+
* <template>
524+
* {{if (eq @status "active") "Active" "Inactive"}}
525+
* </template>
526+
* ```
527+
*
528+
* In strict-mode (gjs/gts) templates, `eq` is available as a keyword and
529+
* does not need to be imported.
530+
*
531+
* @method eq
532+
* @param {unknown} left
533+
* @param {unknown} right
534+
* @return {boolean}
535+
* @public
536+
*/
537+
export const eq = glimmerEq as unknown as EqHelper;
538+
export interface EqHelper extends Opaque<'helper:eq'> {}
539+
540+
/**
541+
* The `{{neq}}` helper returns `true` if its two arguments are strictly
542+
* not equal (`!==`). Takes exactly two arguments.
543+
*
544+
* ```js
545+
* import { neq } from '@ember/helper';
546+
*
547+
* <template>
548+
* {{if (neq @status "active") "Not active" "Active"}}
549+
* </template>
550+
* ```
551+
*
552+
* In strict-mode (gjs/gts) templates, `neq` is available as a keyword and
553+
* does not need to be imported.
554+
*
555+
* @method neq
556+
* @param {unknown} left
557+
* @param {unknown} right
558+
* @return {boolean}
559+
* @public
560+
*/
561+
export const neq = glimmerNeq as unknown as NeqHelper;
562+
export interface NeqHelper extends Opaque<'helper:neq'> {}
563+
514564
/* eslint-enable @typescript-eslint/no-empty-object-type */

packages/@ember/template-compiler/lib/compile-options.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { array, fn, hash } from '@ember/helper';
1+
import { array, eq, fn, hash, neq } from '@ember/helper';
22
import { on } from '@ember/modifier';
33
import { assert } from '@ember/debug';
44
import {
@@ -26,8 +26,10 @@ export const RUNTIME_KEYWORDS_NAME = '__ember_keywords__';
2626

2727
export const keywords: Record<string, unknown> = {
2828
array,
29+
eq,
2930
fn,
3031
hash,
32+
neq,
3133
on,
3234
};
3335

packages/@ember/template-compiler/lib/plugins/auto-import-builtins.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ export default function autoImportBuiltins(env: EmberASTPluginEnvironment): ASTP
3636
if (isHash(node, hasLocal)) {
3737
rewriteKeyword(env, node, 'hash', '@ember/helper');
3838
}
39+
if (isEq(node, hasLocal)) {
40+
rewriteKeyword(env, node, 'eq', '@ember/helper');
41+
}
42+
if (isNeq(node, hasLocal)) {
43+
rewriteKeyword(env, node, 'neq', '@ember/helper');
44+
}
3945
},
4046
MustacheStatement(node: AST.MustacheStatement) {
4147
if (isArray(node, hasLocal)) {
@@ -47,6 +53,12 @@ export default function autoImportBuiltins(env: EmberASTPluginEnvironment): ASTP
4753
if (isHash(node, hasLocal)) {
4854
rewriteKeyword(env, node, 'hash', '@ember/helper');
4955
}
56+
if (isEq(node, hasLocal)) {
57+
rewriteKeyword(env, node, 'eq', '@ember/helper');
58+
}
59+
if (isNeq(node, hasLocal)) {
60+
rewriteKeyword(env, node, 'neq', '@ember/helper');
61+
}
5062
},
5163
},
5264
};
@@ -94,3 +106,17 @@ function isHash(
94106
): node is (AST.MustacheStatement | AST.SubExpression) & { path: AST.PathExpression } {
95107
return isPath(node.path) && node.path.original === 'hash' && !hasLocal('hash');
96108
}
109+
110+
function isEq(
111+
node: AST.MustacheStatement | AST.SubExpression,
112+
hasLocal: (k: string) => boolean
113+
): node is (AST.MustacheStatement | AST.SubExpression) & { path: AST.PathExpression } {
114+
return isPath(node.path) && node.path.original === 'eq' && !hasLocal('eq');
115+
}
116+
117+
function isNeq(
118+
node: AST.MustacheStatement | AST.SubExpression,
119+
hasLocal: (k: string) => boolean
120+
): node is (AST.MustacheStatement | AST.SubExpression) & { path: AST.PathExpression } {
121+
return isPath(node.path) && node.path.original === 'neq' && !hasLocal('neq');
122+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import {
2+
GlimmerishComponent,
3+
jitSuite,
4+
RenderTest,
5+
test,
6+
} from '@glimmer-workspace/integration-tests';
7+
8+
import { template } from '@ember/template-compiler/runtime';
9+
10+
class KeywordEqRuntime extends RenderTest {
11+
static suiteName = 'keyword helper: eq (runtime)';
12+
13+
@test
14+
'explicit scope'() {
15+
const compiled = template('{{if (eq a b) "yes" "no"}}', {
16+
strictMode: true,
17+
scope: () => ({ a: 1, b: 1 }),
18+
});
19+
this.renderComponent(compiled);
20+
this.assertHTML('yes');
21+
}
22+
23+
@test
24+
'explicit scope (shadowed)'() {
25+
const compiled = template('{{if (eq a b) "yes" "no"}}', {
26+
strictMode: true,
27+
scope: () => ({ eq: () => false, a: 1, b: 1 }),
28+
});
29+
this.renderComponent(compiled);
30+
this.assertHTML('no');
31+
}
32+
33+
@test
34+
'implicit scope (eval)'() {
35+
let a = 1;
36+
let b = 1;
37+
hide(a);
38+
hide(b);
39+
const compiled = template('{{if (eq a b) "yes" "no"}}', {
40+
strictMode: true,
41+
eval() {
42+
return eval(arguments[0]);
43+
},
44+
});
45+
this.renderComponent(compiled);
46+
this.assertHTML('yes');
47+
}
48+
49+
@test
50+
'no eval and no scope'() {
51+
class Foo extends GlimmerishComponent {
52+
static {
53+
template('{{if (eq this.a this.b) "yes" "no"}}', {
54+
strictMode: true,
55+
component: this,
56+
});
57+
}
58+
a = 1;
59+
b = 1;
60+
}
61+
this.renderComponent(Foo);
62+
this.assertHTML('yes');
63+
}
64+
}
65+
66+
jitSuite(KeywordEqRuntime);
67+
68+
const hide = (variable: unknown) => {
69+
new Function(`return (${JSON.stringify(variable)});`);
70+
};
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { DEBUG } from '@glimmer/env';
2+
import { jitSuite, RenderTest, test } from '@glimmer-workspace/integration-tests';
3+
4+
import { template } from '@ember/template-compiler';
5+
6+
class KeywordEq extends RenderTest {
7+
static suiteName = 'keyword helper: eq';
8+
9+
@test
10+
'explicit scope'() {
11+
let a = 1;
12+
let b = 1;
13+
14+
const compiled = template('{{eq a b}}', {
15+
strictMode: true,
16+
scope: () => ({ a, b }),
17+
});
18+
19+
this.renderComponent(compiled);
20+
this.assertHTML('true');
21+
}
22+
23+
@test
24+
'explicit scope (shadowed)'() {
25+
let a = 1;
26+
let b = 2;
27+
let eq = () => 'surprise';
28+
const compiled = template('{{eq a b}}', {
29+
strictMode: true,
30+
scope: () => ({ eq, a, b }),
31+
});
32+
33+
this.renderComponent(compiled);
34+
this.assertHTML('surprise');
35+
}
36+
37+
@test
38+
'implicit scope (eval)'() {
39+
let a = 1;
40+
let b = 1;
41+
42+
hide(a);
43+
hide(b);
44+
45+
const compiled = template('{{if (eq a b) "yes" "no"}}', {
46+
strictMode: true,
47+
eval() {
48+
return eval(arguments[0]);
49+
},
50+
});
51+
52+
this.renderComponent(compiled);
53+
this.assertHTML('yes');
54+
}
55+
56+
@test
57+
'returns true for equal numbers'() {
58+
let a = 1;
59+
let b = 1;
60+
const compiled = template('{{if (eq a b) "yes" "no"}}', {
61+
strictMode: true,
62+
scope: () => ({ a, b }),
63+
});
64+
this.renderComponent(compiled);
65+
this.assertHTML('yes');
66+
}
67+
68+
@test
69+
'returns false for unequal numbers'() {
70+
let a = 1;
71+
let b = 2;
72+
const compiled = template('{{if (eq a b) "yes" "no"}}', {
73+
strictMode: true,
74+
scope: () => ({ a, b }),
75+
});
76+
this.renderComponent(compiled);
77+
this.assertHTML('no');
78+
}
79+
80+
@test
81+
'returns true for equal strings'() {
82+
let a = 'hello';
83+
let b = 'hello';
84+
const compiled = template('{{if (eq a b) "yes" "no"}}', {
85+
strictMode: true,
86+
scope: () => ({ a, b }),
87+
});
88+
this.renderComponent(compiled);
89+
this.assertHTML('yes');
90+
}
91+
92+
@test({ skip: !DEBUG })
93+
'throws if not called with exactly two arguments'(assert: Assert) {
94+
let a = 1;
95+
const compiled = template('{{eq a}}', {
96+
strictMode: true,
97+
scope: () => ({ a }),
98+
});
99+
100+
assert.throws(() => {
101+
this.renderComponent(compiled);
102+
}, /`eq` expects exactly two arguments/);
103+
}
104+
}
105+
106+
jitSuite(KeywordEq);
107+
108+
const hide = (variable: unknown) => {
109+
new Function(`return (${JSON.stringify(variable)});`);
110+
};
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import {
2+
GlimmerishComponent,
3+
jitSuite,
4+
RenderTest,
5+
test,
6+
} from '@glimmer-workspace/integration-tests';
7+
8+
import { template } from '@ember/template-compiler/runtime';
9+
10+
class KeywordNeqRuntime extends RenderTest {
11+
static suiteName = 'keyword helper: neq (runtime)';
12+
13+
@test
14+
'explicit scope'() {
15+
const compiled = template('{{if (neq a b) "yes" "no"}}', {
16+
strictMode: true,
17+
scope: () => ({ a: 1, b: 2 }),
18+
});
19+
this.renderComponent(compiled);
20+
this.assertHTML('yes');
21+
}
22+
23+
@test
24+
'explicit scope (shadowed)'() {
25+
const compiled = template('{{if (neq a b) "yes" "no"}}', {
26+
strictMode: true,
27+
scope: () => ({ neq: () => false, a: 1, b: 2 }),
28+
});
29+
this.renderComponent(compiled);
30+
this.assertHTML('no');
31+
}
32+
33+
@test
34+
'implicit scope (eval)'() {
35+
let a = 1;
36+
let b = 2;
37+
hide(a);
38+
hide(b);
39+
const compiled = template('{{if (neq a b) "yes" "no"}}', {
40+
strictMode: true,
41+
eval() {
42+
return eval(arguments[0]);
43+
},
44+
});
45+
this.renderComponent(compiled);
46+
this.assertHTML('yes');
47+
}
48+
49+
@test
50+
'no eval and no scope'() {
51+
class Foo extends GlimmerishComponent {
52+
static {
53+
template('{{if (neq this.a this.b) "yes" "no"}}', {
54+
strictMode: true,
55+
component: this,
56+
});
57+
}
58+
a = 1;
59+
b = 2;
60+
}
61+
this.renderComponent(Foo);
62+
this.assertHTML('yes');
63+
}
64+
}
65+
66+
jitSuite(KeywordNeqRuntime);
67+
68+
const hide = (variable: unknown) => {
69+
new Function(`return (${JSON.stringify(variable)});`);
70+
};

0 commit comments

Comments
 (0)