Skip to content

Commit 65255ad

Browse files
committed
Commit from Devin
1 parent 747a800 commit 65255ad

File tree

4 files changed

+243
-0
lines changed

4 files changed

+243
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { module, test, QUnit } from './utils/qunit';
2+
import { RenderingTest } from './utils/rendering-test';
3+
import { strip } from './utils/strip';
4+
5+
module('Element Helper Tests', () => {
6+
class ElementHelperTest extends RenderingTest {
7+
static suiteName = 'element helper';
8+
9+
@test
10+
'renders a tag with the given tag name'() {
11+
this.render(strip`
12+
{{#let (element "h1") as |Tag|}}
13+
<Tag id="content">hello world!</Tag>
14+
{{/let}}
15+
`);
16+
17+
this.assertHTML('<h1 id="content">hello world!</h1>');
18+
}
19+
20+
@test
21+
'does not render any tags when passed an empty string'() {
22+
this.render(strip`
23+
{{#let (element "") as |Tag|}}
24+
<Tag id="content">hello world!</Tag>
25+
{{/let}}
26+
`);
27+
28+
this.assertHTML('hello world!');
29+
}
30+
31+
@test
32+
'does not render anything when passed null'() {
33+
this.render(strip`
34+
{{#let (element null) as |Tag|}}
35+
<Tag id="content">hello world!</Tag>
36+
{{/let}}
37+
`);
38+
39+
this.assertHTML('<!---->');
40+
}
41+
42+
@test
43+
'does not render anything when passed undefined'() {
44+
this.render(strip`
45+
{{#let (element undefined) as |Tag|}}
46+
<Tag id="content">hello world!</Tag>
47+
{{/let}}
48+
`);
49+
50+
this.assertHTML('<!---->');
51+
}
52+
53+
@test
54+
'works with element modifiers'() {
55+
let clicked = 0;
56+
this.registerHelper('increment', () => () => clicked++);
57+
58+
this.render(strip`
59+
{{#let (element "button") as |Tag|}}
60+
<Tag type="button" id="action" {{on "click" (helper "increment")}}>hello world!</Tag>
61+
{{/let}}
62+
`);
63+
64+
this.assertHTML('<button type="button" id="action">hello world!</button>');
65+
this.assert.strictEqual(clicked, 0, 'never clicked');
66+
67+
this.click('button#action');
68+
this.assert.strictEqual(clicked, 1, 'clicked once');
69+
70+
this.click('button#action');
71+
this.assert.strictEqual(clicked, 2, 'clicked twice');
72+
}
73+
74+
@test
75+
'can be rendered multiple times'() {
76+
this.render(strip`
77+
{{#let (element "h1") as |Tag|}}
78+
<Tag id="content-1">hello</Tag>
79+
<Tag id="content-2">world</Tag>
80+
<Tag id="content-3">!!!!!</Tag>
81+
{{/let}}
82+
`);
83+
84+
this.assertHTML('<h1 id="content-1">hello</h1><h1 id="content-2">world</h1><h1 id="content-3">!!!!!</h1>');
85+
}
86+
87+
@test
88+
'renders when the tag name changes'() {
89+
this.registerHelper('tag-name', () => this.context.tagName);
90+
91+
this.context = { tagName: 'h1' };
92+
this.render(strip`
93+
{{#let (element (helper "tag-name")) as |Tag|}}
94+
<Tag id="content">hello world!</Tag>
95+
{{/let}}
96+
`);
97+
98+
this.assertHTML('<h1 id="content">hello world!</h1>');
99+
100+
this.context = { tagName: 'h2' };
101+
this.rerender();
102+
103+
this.assertHTML('<h2 id="content">hello world!</h2>');
104+
105+
this.context = { tagName: 'h3' };
106+
this.rerender();
107+
108+
this.assertHTML('<h3 id="content">hello world!</h3>');
109+
110+
this.context = { tagName: '' };
111+
this.rerender();
112+
113+
this.assertHTML('hello world!');
114+
115+
this.context = { tagName: 'h1' };
116+
this.rerender();
117+
118+
this.assertHTML('<h1 id="content">hello world!</h1>');
119+
}
120+
121+
@test
122+
'throws when passed a non-string value'() {
123+
this.render(strip`
124+
{{#let (element 123) as |Tag|}}
125+
<Tag id="content">hello world!</Tag>
126+
{{/let}}
127+
`);
128+
129+
this.assertThrows(() => this.rerender(), /The argument passed to the `element` helper must be a string/);
130+
}
131+
}
132+
133+
module('Element Helper Tests', {
134+
beforeEach() {
135+
QUnit.config.current.testEnvironment.owner = {};
136+
}
137+
});
138+
139+
ElementHelperTest.run();
140+
});

packages/@glimmer/runtime/index.ts

+6
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ export {
1717
templateOnlyComponent,
1818
TemplateOnlyComponentManager,
1919
} from './lib/component/template-only';
20+
export {
21+
DynamicTagComponent,
22+
DynamicTagComponentManager,
23+
DYNAMIC_TAG_COMPONENT_MANAGER,
24+
} from './lib/component/dynamic-tag';
2025
export { CurriedValue, curry } from './lib/curried-value';
2126
export {
2227
DOMChanges,
@@ -33,6 +38,7 @@ export {
3338
} from './lib/environment';
3439
export { array } from './lib/helpers/array';
3540
export { concat } from './lib/helpers/concat';
41+
export { element } from './lib/helpers/element';
3642
export { fn } from './lib/helpers/fn';
3743
export { get } from './lib/helpers/get';
3844
export { hash } from './lib/helpers/hash';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { InternalComponentCapabilities, InternalComponentManager, Option, Destroyable } from '@glimmer/interfaces';
2+
import { setInternalComponentManager } from '@glimmer/manager';
3+
import { NULL_REFERENCE, Reference } from '@glimmer/reference';
4+
5+
const CAPABILITIES: InternalComponentCapabilities = {
6+
dynamicLayout: false,
7+
dynamicTag: true,
8+
prepareArgs: false,
9+
createArgs: false,
10+
attributeHook: false,
11+
elementHook: false,
12+
createCaller: false,
13+
dynamicScope: false,
14+
updateHook: false,
15+
createInstance: true,
16+
wrapped: false,
17+
willDestroy: false,
18+
hasSubOwner: false,
19+
};
20+
21+
export class DynamicTagComponent {
22+
constructor(public tagName: string) {}
23+
}
24+
25+
export class DynamicTagComponentManager implements InternalComponentManager {
26+
getCapabilities(): InternalComponentCapabilities {
27+
return CAPABILITIES;
28+
}
29+
30+
getDebugName(): string {
31+
return 'dynamic-tag';
32+
}
33+
34+
getSelf(): Reference {
35+
return NULL_REFERENCE;
36+
}
37+
38+
getDestroyable(): Option<Destroyable> {
39+
return null;
40+
}
41+
}
42+
43+
export const DYNAMIC_TAG_COMPONENT_MANAGER = new DynamicTagComponentManager();
44+
45+
setInternalComponentManager(DYNAMIC_TAG_COMPONENT_MANAGER, DynamicTagComponent.prototype);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { CapturedArguments } from '@glimmer/interfaces';
2+
import { createComputeRef, valueForRef } from '@glimmer/reference';
3+
import { curry } from '../curried-value';
4+
import { internalHelper } from './internal-helper';
5+
import { CurriedType } from '@glimmer/interfaces';
6+
7+
/**
8+
Use the `{{element}}` helper to create a contextual component with a dynamic tag name.
9+
10+
```handlebars
11+
{{#let (element "div") as |Tag|}}
12+
<Tag>hello</Tag>
13+
{{/let}}
14+
```
15+
16+
You can also pass a dynamic value as the tag name:
17+
18+
```handlebars
19+
{{#let (element (if this.isActive "button" "div")) as |Tag|}}
20+
<Tag>hello</Tag>
21+
{{/let}}
22+
```
23+
24+
@method element
25+
@param {String} tagName The HTML tag name to render
26+
@return {Component} A component with the specified tag name
27+
@public
28+
*/
29+
export const element = internalHelper(({ positional }: CapturedArguments) => {
30+
return createComputeRef(() => {
31+
const tagName = positional[0] ? valueForRef(positional[0]) : undefined;
32+
33+
// Handle null, undefined, or empty string
34+
if (tagName === null || tagName === undefined || tagName === '') {
35+
return undefined;
36+
}
37+
38+
// Validate tag name is a string
39+
if (typeof tagName !== 'string') {
40+
throw new Error(`The argument passed to the \`element\` helper must be a string (you passed \`${tagName}\`)`);
41+
}
42+
43+
// Create a dynamic component with the specified tag name
44+
return curry(
45+
CurriedType.Component,
46+
{ tagName },
47+
{},
48+
null,
49+
false
50+
);
51+
}, null, 'element');
52+
});

0 commit comments

Comments
 (0)