Skip to content

Commit b2eb92d

Browse files
committed
RFC#389 - {{element}} as keyword
Register element as a built-in keyword so it no longer needs to be imported in strict-mode (gjs/gts) templates.
1 parent 8c2698d commit b2eb92d

5 files changed

Lines changed: 190 additions & 1 deletion

File tree

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { array, eq, fn, hash, neq } from '@ember/helper';
1+
import { array, element, eq, fn, hash, neq } from '@ember/helper';
22
import { on } from '@ember/modifier';
33
import { assert } from '@ember/debug';
44
import {
@@ -27,6 +27,7 @@ export const RUNTIME_KEYWORDS_NAME = '__ember_keywords__';
2727
export const keywords: Record<string, unknown> = {
2828
array,
2929
eq,
30+
element,
3031
fn,
3132
hash,
3233
neq,

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ export default function autoImportBuiltins(env: EmberASTPluginEnvironment): ASTP
3030
if (isArray(node, hasLocal)) {
3131
rewriteKeyword(env, node, 'array', '@ember/helper');
3232
}
33+
if (isElement(node, hasLocal)) {
34+
rewriteKeyword(env, node, 'element', '@ember/helper');
35+
}
3336
if (isFn(node, hasLocal)) {
3437
rewriteKeyword(env, node, 'fn', '@ember/helper');
3538
}
@@ -47,6 +50,9 @@ export default function autoImportBuiltins(env: EmberASTPluginEnvironment): ASTP
4750
if (isArray(node, hasLocal)) {
4851
rewriteKeyword(env, node, 'array', '@ember/helper');
4952
}
53+
if (isElement(node, hasLocal)) {
54+
rewriteKeyword(env, node, 'element', '@ember/helper');
55+
}
5056
if (isFn(node, hasLocal)) {
5157
rewriteKeyword(env, node, 'fn', '@ember/helper');
5258
}
@@ -120,3 +126,10 @@ function isNeq(
120126
): node is (AST.MustacheStatement | AST.SubExpression) & { path: AST.PathExpression } {
121127
return isPath(node.path) && node.path.original === 'neq' && !hasLocal('neq');
122128
}
129+
130+
function isElement(
131+
node: AST.MustacheStatement | AST.SubExpression,
132+
hasLocal: (k: string) => boolean
133+
): node is (AST.MustacheStatement | AST.SubExpression) & { path: AST.PathExpression } {
134+
return isPath(node.path) && node.path.original === 'element' && !hasLocal('element');
135+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { castToBrowser } from '@glimmer/debug-util';
2+
import {
3+
GlimmerishComponent,
4+
jitSuite,
5+
RenderTest,
6+
test,
7+
} from '@glimmer-workspace/integration-tests';
8+
9+
import { template } from '@ember/template-compiler/runtime';
10+
11+
class KeywordElementRuntime extends RenderTest {
12+
static suiteName = 'keyword helper: element (runtime)';
13+
14+
@test
15+
'explicit scope'(assert: Assert) {
16+
const compiled = template('{{#let (element "h1") as |Tag|}}<Tag>Hello</Tag>{{/let}}', {
17+
strictMode: true,
18+
scope: () => ({}),
19+
});
20+
21+
this.renderComponent(compiled);
22+
23+
let h1 = castToBrowser(this.element, 'div').querySelector('h1');
24+
assert.ok(h1, 'h1 element exists');
25+
assert.strictEqual(h1!.textContent, 'Hello');
26+
}
27+
28+
@test
29+
'explicit scope (shadowed)'() {
30+
const compiled = template('{{element "h1"}}', {
31+
strictMode: true,
32+
scope: () => ({ element: () => 'surprise' }),
33+
});
34+
35+
this.renderComponent(compiled);
36+
this.assertHTML('surprise');
37+
}
38+
39+
@test
40+
'implicit scope (eval)'(assert: Assert) {
41+
const compiled = template('{{#let (element "h1") as |Tag|}}<Tag>Hello</Tag>{{/let}}', {
42+
strictMode: true,
43+
eval() {
44+
return eval(arguments[0]);
45+
},
46+
});
47+
48+
this.renderComponent(compiled);
49+
50+
let h1 = castToBrowser(this.element, 'div').querySelector('h1');
51+
assert.ok(h1, 'h1 element exists');
52+
assert.strictEqual(h1!.textContent, 'Hello');
53+
}
54+
55+
@test
56+
'no eval and no scope'(assert: Assert) {
57+
class Foo extends GlimmerishComponent {
58+
static {
59+
template('{{#let (element "h1") as |Tag|}}<Tag>Hello</Tag>{{/let}}', {
60+
strictMode: true,
61+
component: this,
62+
});
63+
}
64+
}
65+
66+
this.renderComponent(Foo);
67+
68+
let h1 = castToBrowser(this.element, 'div').querySelector('h1');
69+
assert.ok(h1, 'h1 element exists');
70+
assert.strictEqual(h1!.textContent, 'Hello');
71+
}
72+
}
73+
74+
jitSuite(KeywordElementRuntime);
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { castToBrowser } from '@glimmer/debug-util';
2+
import { jitSuite, RenderTest, test } from '@glimmer-workspace/integration-tests';
3+
4+
import { template } from '@ember/template-compiler';
5+
6+
class KeywordElement extends RenderTest {
7+
static suiteName = 'keyword helper: element';
8+
9+
@test
10+
'explicit scope'(assert: Assert) {
11+
const compiled = template('{{#let (element "h1") as |Tag|}}<Tag>Hello</Tag>{{/let}}', {
12+
strictMode: true,
13+
scope: () => ({}),
14+
});
15+
16+
this.renderComponent(compiled);
17+
18+
let h1 = castToBrowser(this.element, 'div').querySelector('h1');
19+
assert.ok(h1, 'h1 element exists');
20+
assert.strictEqual(h1!.textContent, 'Hello');
21+
}
22+
23+
@test
24+
'explicit scope (shadowed)'() {
25+
let element = () => 'surprise';
26+
const compiled = template('{{element "h1"}}', {
27+
strictMode: true,
28+
scope: () => ({ element }),
29+
});
30+
31+
this.renderComponent(compiled);
32+
this.assertHTML('surprise');
33+
}
34+
35+
@test
36+
'implicit scope (eval)'(assert: Assert) {
37+
const compiled = template('{{#let (element "h1") as |Tag|}}<Tag>Hello</Tag>{{/let}}', {
38+
strictMode: true,
39+
eval() {
40+
return eval(arguments[0]);
41+
},
42+
});
43+
44+
this.renderComponent(compiled);
45+
46+
let h1 = castToBrowser(this.element, 'div').querySelector('h1');
47+
assert.ok(h1, 'h1 element exists');
48+
assert.strictEqual(h1!.textContent, 'Hello');
49+
}
50+
51+
@test
52+
MustacheStatement(assert: Assert) {
53+
const Child = template('{{#let @tag as |Tag|}}<Tag>World</Tag>{{/let}}', {
54+
strictMode: true,
55+
scope: () => ({}),
56+
});
57+
58+
const compiled = template('<Child @tag={{element "span"}} />', {
59+
strictMode: true,
60+
scope: () => ({ Child }),
61+
});
62+
63+
this.renderComponent(compiled);
64+
65+
let span = castToBrowser(this.element, 'div').querySelector('span');
66+
assert.ok(span, 'span element exists');
67+
assert.strictEqual(span!.textContent, 'World');
68+
}
69+
}
70+
71+
jitSuite(KeywordElement);

smoke-tests/scenarios/basic-test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,36 @@ function basicTest(scenarios: Scenarios, appName: string) {
442442
});
443443
});
444444
`,
445+
'element-as-keyword-test.gjs': `
446+
import { module, test } from 'qunit';
447+
import { setupRenderingTest } from 'ember-qunit';
448+
import { render } from '@ember/test-helpers';
449+
450+
module('{{element}} as keyword', function(hooks) {
451+
setupRenderingTest(hooks);
452+
453+
test('it works', async function(assert) {
454+
await render(
455+
<template>
456+
{{#let (element "h1") as |Tag|}}
457+
<Tag class="greeting">Hello from element keyword</Tag>
458+
{{/let}}
459+
</template>
460+
);
461+
assert.dom('h1.greeting').hasText('Hello from element keyword');
462+
});
463+
464+
test('can be shadowed', async function(assert) {
465+
let element = () => 'surprise';
466+
await render(
467+
<template>
468+
<span data-test>{{element "h1"}}</span>
469+
</template>
470+
);
471+
assert.dom('[data-test]').hasText('surprise');
472+
});
473+
});
474+
`,
445475
'fn-as-keyword-but-its-shadowed-test.gjs': `
446476
import QUnit, { module, test } from 'qunit';
447477
import { setupRenderingTest } from 'ember-qunit';

0 commit comments

Comments
 (0)