Skip to content

Commit 747a800

Browse files
committedMar 17, 2025
from Devin: try to implement the element keyword
1 parent 809e52a commit 747a800

File tree

6 files changed

+266
-0
lines changed

6 files changed

+266
-0
lines changed
 

‎packages/@glimmer/compiler/lib/passes/1-normalization/keywords/block.ts

+55
Original file line numberDiff line numberDiff line change
@@ -382,4 +382,59 @@ export const BLOCK_KEYWORDS = keywords('Block')
382382
})
383383
);
384384
},
385+
})
386+
.kw('element', {
387+
assert(node: ASTv2.InvokeBlock): Result<ASTv2.ExpressionNode> {
388+
let { args } = node;
389+
390+
if (!args.named.isEmpty()) {
391+
return Err(
392+
generateSyntaxError(
393+
`The element keyword does not take any named arguments`,
394+
args.named.loc
395+
)
396+
);
397+
}
398+
399+
if (args.positional.size !== 1) {
400+
return Err(
401+
generateSyntaxError(
402+
`The element keyword only takes a single positional parameter, the tag name.`,
403+
args.positional.loc
404+
)
405+
);
406+
}
407+
408+
let tagName = args.nth(0);
409+
410+
if (tagName === null) {
411+
return Err(
412+
generateSyntaxError(
413+
`The element keyword requires a tag name as its first positional parameter`,
414+
args.loc
415+
)
416+
);
417+
}
418+
419+
return Ok(tagName);
420+
},
421+
422+
translate(
423+
{ node, state }: { node: ASTv2.InvokeBlock; state: NormalizationState },
424+
tagName: ASTv2.ExpressionNode
425+
): Result<mir.DynamicElement> {
426+
let block = node.blocks.get('default');
427+
let body = VISIT_STMTS.NamedBlock(block, state);
428+
let tagNameResult = VISIT_EXPRS.visit(tagName, state);
429+
430+
return Result.all(body, tagNameResult).mapOk(
431+
([body, tag]) =>
432+
new mir.DynamicElement({
433+
loc: node.loc,
434+
tag,
435+
params: new mir.ElementParameters({ body: [] }),
436+
body: body.body,
437+
})
438+
);
439+
},
385440
});

‎packages/@glimmer/compiler/lib/passes/2-encoding/mir.ts

+7
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@ export class SimpleElement extends node('SimpleElement').fields<{
120120
dynamicFeatures: boolean;
121121
}>() {}
122122

123+
export class DynamicElement extends node('DynamicElement').fields<{
124+
tag: ExpressionNode;
125+
params: ElementParameters;
126+
body: Statement[];
127+
}>() {}
128+
123129
export class ElementParameters extends node('ElementParameters').fields<{
124130
body: AnyOptionalList<ElementParameter>;
125131
}>() {}
@@ -214,6 +220,7 @@ export type Statement =
214220
| AppendTextNode
215221
| Component
216222
| SimpleElement
223+
| DynamicElement
217224
| InvokeBlock
218225
| AppendComment
219226
| If
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { preprocess, print } from '@glimmer/syntax';
2+
import { compileTemplate } from '../../index';
3+
import { expect } from 'chai';
4+
import { suite, test } from 'qunit';
5+
6+
suite('Element Keyword', () => {
7+
test('it renders a tag with the given tag name', (assert) => {
8+
const template = `
9+
{{#element "h1"}}
10+
hello world!
11+
{{/element}}
12+
`;
13+
14+
const result = compileTemplate(template);
15+
assert.ok(result, 'Successfully compiled template with element keyword');
16+
17+
// Verify the compiled template contains the expected opcodes
18+
const serialized = JSON.stringify(result);
19+
assert.ok(serialized.includes('OpenDynamicElement'), 'Template includes OpenDynamicElement opcode');
20+
});
21+
22+
test('it handles dynamic tag names', (assert) => {
23+
const template = `
24+
{{#element tagName}}
25+
hello world!
26+
{{/element}}
27+
`;
28+
29+
const result = compileTemplate(template);
30+
assert.ok(result, 'Successfully compiled template with dynamic element keyword');
31+
32+
// Verify the compiled template contains the expected opcodes
33+
const serialized = JSON.stringify(result);
34+
assert.ok(serialized.includes('OpenDynamicElement'), 'Template includes OpenDynamicElement opcode');
35+
});
36+
37+
test('it handles null/undefined tag names', (assert) => {
38+
const template = `
39+
{{#element null}}
40+
hello world!
41+
{{/element}}
42+
`;
43+
44+
const result = compileTemplate(template);
45+
assert.ok(result, 'Successfully compiled template with null element keyword');
46+
});
47+
48+
test('it supports element modifiers', (assert) => {
49+
const template = `
50+
{{#element "button" type="button" {{on "click" this.didClick}}}}
51+
hello world!
52+
{{/element}}
53+
`;
54+
55+
const result = compileTemplate(template);
56+
assert.ok(result, 'Successfully compiled template with element modifiers');
57+
58+
// Verify the compiled template contains the expected opcodes
59+
const serialized = JSON.stringify(result);
60+
assert.ok(serialized.includes('OpenDynamicElement'), 'Template includes OpenDynamicElement opcode');
61+
assert.ok(serialized.includes('Modifier'), 'Template includes Modifier opcode');
62+
});
63+
64+
test('it throws an error for invalid tag names', (assert) => {
65+
const template = `
66+
{{#element 123}}
67+
hello world!
68+
{{/element}}
69+
`;
70+
71+
assert.throws(() => {
72+
compile(template);
73+
}, /The element keyword only takes a single positional parameter, the tag name/);
74+
});
75+
76+
test('it throws an error when no arguments are provided', (assert) => {
77+
const template = `
78+
{{#element}}
79+
hello world!
80+
{{/element}}
81+
`;
82+
83+
assert.throws(() => {
84+
compile(template);
85+
}, /The element keyword requires a tag name as its first positional parameter/);
86+
});
87+
88+
test('it throws an error when too many arguments are provided', (assert) => {
89+
const template = `
90+
{{#element "div" "span"}}
91+
hello world!
92+
{{/element}}
93+
`;
94+
95+
assert.throws(() => {
96+
compile(template);
97+
}, /The element keyword only takes a single positional parameter, the tag name/);
98+
});
99+
100+
test('it throws an error when named arguments are provided', (assert) => {
101+
const template = `
102+
{{#element "div" id="test"}}
103+
hello world!
104+
{{/element}}
105+
`;
106+
107+
assert.throws(() => {
108+
compile(template);
109+
}, /The element keyword does not take any named arguments/);
110+
});
111+
});

‎packages/@glimmer/opcode-compiler/lib/syntax/statements.ts

+23
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,29 @@ STATEMENTS.add(SexpOpcodes.OpenElement, (op, [, tag]) => {
142142
op(VM_OPEN_ELEMENT_OP, inflateTagName(tag));
143143
});
144144

145+
STATEMENTS.add(SexpOpcodes.DynamicElement, (op, [, tag, params, body]) => {
146+
expr(op, tag);
147+
op(VM_OPEN_DYNAMIC_ELEMENT_OP);
148+
149+
if (params && params.length > 0) {
150+
op(VM_PUT_COMPONENT_OPERATIONS_OP);
151+
152+
for (let param of params) {
153+
STATEMENTS.compile(op, param);
154+
}
155+
}
156+
157+
op(VM_FLUSH_ELEMENT_OP);
158+
159+
if (body && body.length > 0) {
160+
for (let statement of body) {
161+
STATEMENTS.compile(op, statement);
162+
}
163+
}
164+
165+
op(VM_CLOSE_ELEMENT_OP);
166+
});
167+
145168
STATEMENTS.add(SexpOpcodes.OpenElementWithSplat, (op, [, tag]) => {
146169
op(VM_PUT_COMPONENT_OPERATIONS_OP);
147170
op(VM_OPEN_ELEMENT_OP, inflateTagName(tag));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { CurriedType, Dict, Maybe, InternalComponentCapabilities, InternalComponentManager, Option, Destroyable } from '@glimmer/interfaces';
2+
import { createComputeRef, Reference, valueForRef, NULL_REFERENCE } from '@glimmer/reference';
3+
import { setInternalComponentManager } from '@glimmer/manager';
4+
import { curry } from '../curried-value';
5+
6+
const CAPABILITIES: InternalComponentCapabilities = {
7+
dynamicLayout: false,
8+
dynamicTag: true,
9+
prepareArgs: false,
10+
createArgs: false,
11+
attributeHook: false,
12+
elementHook: false,
13+
createCaller: false,
14+
dynamicScope: false,
15+
updateHook: false,
16+
createInstance: true,
17+
wrapped: false,
18+
willDestroy: false,
19+
hasSubOwner: false,
20+
};
21+
22+
class DynamicElementComponent {
23+
constructor(public tagName: string) {}
24+
}
25+
26+
export class DynamicElementManager implements InternalComponentManager {
27+
getCapabilities(): InternalComponentCapabilities {
28+
return CAPABILITIES;
29+
}
30+
31+
getDebugName(): string {
32+
return 'dynamic-element';
33+
}
34+
35+
getSelf(): Reference {
36+
return NULL_REFERENCE;
37+
}
38+
39+
getDestroyable(): Option<Destroyable> {
40+
return null;
41+
}
42+
}
43+
44+
export const DYNAMIC_ELEMENT_MANAGER = new DynamicElementManager();
45+
46+
setInternalComponentManager(DYNAMIC_ELEMENT_MANAGER, DynamicElementComponent);
47+
48+
export default function createDynamicElementRef(tagName: Reference) {
49+
let lastValue: Maybe<Dict> | string, curriedDefinition: object | string | null;
50+
51+
return createComputeRef(() => {
52+
let value = valueForRef(tagName) as Maybe<Dict> | string;
53+
54+
if (value === lastValue) {
55+
return curriedDefinition;
56+
}
57+
58+
lastValue = value;
59+
curriedDefinition = curry(
60+
CurriedType.Component,
61+
new DynamicElementComponent(tagName),
62+
{},
63+
null,
64+
false
65+
);
66+
67+
return curriedDefinition;
68+
});
69+
}

‎packages/@glimmer/wire-format/lib/opcodes.ts

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export const opcodes = {
5656
Block: 6 satisfies BlockOpcode,
5757
StrictBlock: 7 satisfies StrictBlockOpcode,
5858
Component: 8 satisfies ComponentOpcode,
59+
DynamicElement: 9 satisfies number,
5960
OpenElement: 10 satisfies OpenElementOpcode,
6061
OpenElementWithSplat: 11 satisfies OpenElementWithSplatOpcode,
6162
FlushElement: 12 satisfies FlushElementOpcode,

0 commit comments

Comments
 (0)