Skip to content

Commit fc0036b

Browse files
authored
Merge pull request #1978 from IgnaceMaes/template-tag-guides
docs: Add Template Tag format guides
2 parents 708c00b + 41bd12b commit fc0036b

File tree

4 files changed

+336
-1
lines changed

4 files changed

+336
-1
lines changed

.local.dic

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ instantiation
109109
Intellisense
110110
IntelliSense
111111
interop
112+
integrations
112113
interoperable
113114
invokable
114115
invokables
@@ -137,6 +138,7 @@ nav
137138
nav-bar
138139
Neovim
139140
NVDA
141+
nvim-treesitter
140142
onboarding
141143
Orca
142144
page-crafter
@@ -207,6 +209,7 @@ TalkBack
207209
teardown
208210
template-lifecycle-dom-and-modifiers
209211
templating
212+
TextMate
210213
todo
211214
todos
212215
tooltip
@@ -237,6 +240,7 @@ VM
237240
VoiceOver
238241
voilà
239242
Voilà
243+
vscode-glimmer-syntax
240244
websocket
241245
working-with-html-css-and-javascript
242246
yay
Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
The template tag format is a powerful, new way to write components in Ember. It's a single-file format that combines the component's JavaScript and Glimmer template code. The `<template>` tag is used to keep a clear separation between the template language and the JavaScript around it.
2+
3+
Template tag components use the file extension `.gjs`. This abbreviation is short for "Glimmer JavaScript". The file extension `.gts` is also supported for TypeScript components.
4+
5+
This new format is [the official future of Ember's component authoring story](https://rfcs.emberjs.com/id/0779-first-class-component-templates/), and is stable and usable today. The RFC is currently in the "Accepted" stage, and work is ongoing to get it to "Ready for Release". We expect it to become the recommended and default way of authoring all Ember apps in the near future, once we are satisfied that we have sufficiently polished up all the corners of the implementation.
6+
7+
> Can't wait to get started? Head over to the [installation section](#toc_installation) to begin using template tag components in your apps and addons today.
8+
9+
## Writing template tag components
10+
11+
Just like with separate JavaScript and Glimmer template files, the template tag format has the concept of template-only components and class-based components. Let's take a closer look at how these concepts compare between both component formats in the next section.
12+
13+
### Template-only components
14+
15+
The following template-only component was created in a [previous section](../component-arguments-and-html-attributes/) to extract an avatar layout into a reusable component.
16+
17+
```handlebars {data-filename="app/components/avatar.hbs"}
18+
<aside>
19+
<div class="avatar" title={{@title}}>{{@initial}}</div>
20+
</aside>
21+
```
22+
23+
This layout can be turned into a template tag component by wrapping the code in a `<template>` tag and changing the file extension to `.gjs`.
24+
25+
```text {data-filename="app/components/avatar.gjs"}
26+
<template>
27+
<aside>
28+
<div class="avatar" title={{@title}}>{{@initial}}</div>
29+
</aside>
30+
</template>
31+
```
32+
33+
The top-level template tag is exported as the default component from the file. You *can* write this export explicitly, but it's not necessary. The following example is equivalent to the previous one.
34+
35+
```text {data-filename="app/components/avatar.gjs"}
36+
export default <template>
37+
<aside>
38+
<div class="avatar" title={{@title}}>{{@initial}}</div>
39+
</aside>
40+
</template>;
41+
```
42+
43+
### Class-based components
44+
45+
A `<template>` tag can also be embedded inside a class definition of a component. This is useful when you need to add state or other logic to your component. Take for example the following "Avatar" component, where a default title is added when the `title` argument is not provided.
46+
47+
```text {data-filename="app/components/avatar.gjs"}
48+
import Component from '@glimmer/component';
49+
50+
export default class Avatar extends Component {
51+
get titleWithDefault() {
52+
return this.args.title ?? 'No avatar title provided';
53+
}
54+
55+
<template>
56+
<aside>
57+
<div class="avatar" title={{this.titleWithDefault}}>{{@initial}}</div>
58+
</aside>
59+
</template>
60+
}
61+
```
62+
63+
## Importing components, helpers, and modifiers
64+
65+
In Ember templates, **“invokables”** are things you can *invoke* in a template. These include [components](./introducing-components/), [helpers](./helper-functions/), and [modifiers](./template-lifecycle-dom-and-modifiers/). In the template tag format, these invokables need to be imported before they can be used. This makes it easier to understand where values come from and what they do, as well as unlocks build optimizations.
66+
67+
68+
### Importing invokables from your own app
69+
70+
When making use of the "Avatar" component as defined before in a different component file, it first needs to be imported. This is done using the `import` statement, just like you would import any other JavaScript module.
71+
72+
```text {data-filename="app/components/message.gjs"}
73+
import Avatar from './avatar';
74+
75+
<template>
76+
<Avatar
77+
@title={{@avatarTitle}}
78+
@initial={{@avatarInitial}}
79+
/>
80+
<section>
81+
{{@message}}
82+
</section>
83+
</template>
84+
```
85+
86+
The example above demonstrates defining a "Message" template-only component. The import syntax for class-based components is the same.
87+
88+
<div class="cta">
89+
<div class="cta-note">
90+
<div class="cta-note-body">
91+
<div class="cta-note-heading">Zoey says...</div>
92+
<div class="cta-note-message">
93+
The components that are imported are not required to use the new template tag format. This is intentional, and very powerful, as it <strong>allows incremental adoption</strong> of the new format.
94+
<br><br>
95+
The only prerequisite is that the component is defined using the <a href="https://rfcs.emberjs.com/id/0481-component-templates-co-location">template-colocation structure</a> instead of splitting up the JavaScript and Glimmer template files into separate folders.
96+
</div>
97+
</div>
98+
<img src="/images/mascots/zoey.png" role="presentation" alt="">
99+
</div>
100+
</div>
101+
102+
#### Nested components
103+
104+
Component files can be organized in nested directory structures on the file system. Prior to the template tag format, the file path from the root component directory had be specified before to the component name, separated with `::`.
105+
106+
For example, when moving the "Avatar" component to the `app/components/messages` namespace, referencing it using double colons would be done as follows.
107+
108+
```handlebars {data-filename="app/components/avatar-usage.hbs"}
109+
<Messages::Avatar
110+
@title="Picture of Zoey"
111+
@initial="Zoey"
112+
/>
113+
```
114+
115+
This quirk is no longer necessary with the template tag format. Instead, importing now works the same as importing any other JavaScript module.
116+
117+
```text {data-filename="app/components/avatar-usage.gjs"}
118+
import Avatar from './messages/avatar';
119+
120+
<template>
121+
<Avatar
122+
@title="Picture of Zoey"
123+
@initial="Zoey"
124+
/>
125+
</template>
126+
```
127+
128+
#### Helpers and modifiers
129+
130+
Importing helpers and modifiers from your own app also follows the same principle of using standard JavaScript import syntax. Instead of importing from `app/components`, the path to import from is `app/helpers` and `app/modifiers` respectively.
131+
132+
Prior to the template tag format, helpers and modifiers were referenced based on their name in the "kebab-case" convention. For example, a `randomNumber` function as helper would be referenced as `{{random-number}}` in a template. In the new way of doing things, standard module import conventions are used. This means that the helper is referenced using the name it is exported as, which is `randomNumber` in this case.
133+
134+
```text {data-filename="app/components/random-number.gjs"}
135+
import randomNumber from '../helpers/random-number';
136+
137+
<template>
138+
{{randomNumber}}
139+
</template>
140+
```
141+
142+
### Importing from addons
143+
144+
Just as with components, helpers, and modifiers from your own app, external invokables from addons also have to be imported. This is done using the same `import` statement, but with a path referencing the addon.
145+
146+
The structure of files within Ember addons is mostly standardized. This means that the path to import from can be derived from the addon's name. For example, an addon that is named `ember-foo` will likely have its components, helpers, and modifiers available as default import from the following locations:
147+
148+
```text
149+
ember-foo/components/example-component
150+
ember-foo/helpers/example-helper
151+
ember-foo/modifiers/example-modifier
152+
```
153+
154+
To import the "ExampleComponent" component from the `ember-foo` addon, the following import statement can be used.
155+
156+
```js
157+
import ExampleComponent from 'ember-foo/components/example-component';
158+
```
159+
160+
Some addons may choose to re-export their invokables from the root index as named exports. Usually addons will document this usage in their README, if supported, which may look like:
161+
162+
```js
163+
import { ExampleComponent } from 'ember-foo';
164+
```
165+
166+
### Importing built-ins
167+
168+
The Ember built-in helpers, modifiers, and components are available for import from the following locations.
169+
170+
```js
171+
// Built-in helpers
172+
import { array } from '@ember/helper';
173+
import { concat } from '@ember/helper';
174+
import { fn } from '@ember/helper';
175+
import { get } from '@ember/helper';
176+
import { hash } from '@ember/helper';
177+
178+
// Built-in modifiers
179+
import { on } from '@ember/modifier';
180+
181+
// Built-in components
182+
import { Input } from '@ember/component';
183+
import { LinkTo } from '@ember/routing';
184+
import { Textarea } from '@ember/component';
185+
```
186+
187+
#### Keywords
188+
189+
While most items should be imported into scope explicitly, some of the existing constructs in the language are not importable and are available as keywords instead:
190+
191+
`action`, `debugger`, `each-in`, `each`, `has-block-params`, `has-block`, `hasBlock`, `if`, `in-element`, `let`, `link-to` (non-block form curly invocations), `loc`, `log`, `mount`, `mut`, `outlet`, `query-params`, `readonly`, `unbound`, `unless`, `with`, and `yield`
192+
193+
These keywords do not have to be imported into scope and will always be available.
194+
195+
<div class="cta">
196+
<div class="cta-note">
197+
<div class="cta-note-body">
198+
<div class="cta-note-heading">Zoey says...</div>
199+
<div class="cta-note-message">
200+
Feeling a bit lost with remembering all import paths?
201+
<br><br>
202+
Make sure to look at your editor setup to see if it can help you with auto-completion of import paths. See the <a href="#toc_editor-integrations">Editor Integrations</a> section for more information.
203+
</div>
204+
</div>
205+
<img src="/images/mascots/zoey.png" role="presentation" alt="">
206+
</div>
207+
</div>
208+
209+
## New capabilities
210+
211+
In the examples above, functionality that was already available before was covered using the template tag format. The template tag format, however, unlocks a number of new capabilities that were not possible before.
212+
213+
### Locally-scoped values
214+
215+
The template tag format follows JavaScript module syntax. Any value that isn't exported is only available locally within the file. This is useful for defining helper functions that are only used within the component, or for defining constants that are used multiple times within the template.
216+
217+
In the following example, a "Square" component is defined that calculates the square of a number. The `value` constant is defined locally, and the `square` helper function is only available within the component.
218+
219+
```text {data-filename="app/components/square.gjs"}
220+
const value = 2;
221+
222+
function square(number) {
223+
return number * number;
224+
}
225+
226+
<template>
227+
The square of {{value}} equals {{square value}}
228+
</template>
229+
```
230+
231+
This will render to `The square of 2 equals 4`.
232+
233+
### Multiple components per file
234+
235+
The template tag format allows defining multiple components within a single file. This is useful for defining components that are closely related to each other, but are not used in other parts of the app.
236+
237+
The following example defines a "CustomSelect" component that renders a `<select>` element with a list of options. The locally-defined "Option" component is used to render each option in the list.
238+
239+
```text {data-filename="app/components/custom-select.gjs"}
240+
const Option = <template>
241+
<option selected={{@selected}} value={{@value}}>
242+
{{@value}}
243+
</option>
244+
</template>;
245+
246+
const CustomSelect = <template>
247+
<select>
248+
{{#each @options as |opt|}}
249+
<Option
250+
@value={{opt.value}}
251+
@selected={{eq opt @selectedOption}}
252+
/>
253+
{{/each}}
254+
</select>
255+
</template>;
256+
257+
export default CustomSelect;
258+
```
259+
260+
This can be a powerful refactoring technique to break up large components into smaller ones. (where it makes sense!)
261+
262+
## Testing
263+
264+
Historically, Ember's integration tests have been written using the `hbs` tagged template literal. This is no longer necessary with the template tag format. Instead, use the `<template>` tag to define a template to render.
265+
266+
The following example showcases how the "Avatar" component can be tested using the template tag format.
267+
268+
```text {data-filename="tests/integration/components/avatar-test.gjs"}
269+
import Avatar from 'app/components/avatar';
270+
import { module, test } from 'qunit';
271+
import { setupRenderingTest } from 'ember-qunit';
272+
import { render } from '@ember/test-helpers';
273+
274+
module('Integration | Component | avatar', function (hooks) {
275+
setupRenderingTest(hooks);
276+
277+
test('renders name argument', async function (assert) {
278+
const initial = 'Zoey';
279+
await render(
280+
<template>
281+
<Avatar @title="Picture of Zoey" @initial={{initial}} />
282+
</template>
283+
);
284+
assert.dom().hasText(initial);
285+
});
286+
});
287+
```
288+
289+
Notice how the same semantics now apply to tests as well: local values in scope can be referenced directly, and invokables from your own app or addons need to be imported.
290+
291+
## Installation
292+
293+
Install the [ember-template-imports](https://github.com/ember-template-imports/ember-template-imports) addon to start using template tag components. This addon provides all the build tooling required to support the new component authoring format.
294+
295+
```bash
296+
npm add --save-dev ember-template-imports
297+
```
298+
299+
### Integration with external tooling
300+
301+
You may need to upgrade dependency versions or install additional plugins to have proper integration with external tools. The following commonly-used tools are supported:
302+
303+
- [ember-template-lint](https://github.com/ember-template-lint/ember-template-lint): Versions 5.8.0 and up.
304+
- [eslint-plugin-ember](https://github.com/ember-cli/eslint-plugin-ember): Versions 11.6.0 and up.
305+
- [Prettier](https://github.com/prettier/prettier): Versions 3.1.0 and up. This requires installing the [prettier-plugin-ember-template-tag](https://github.com/gitKrystan/prettier-plugin-ember-template-tag).
306+
- [Glint](https://github.com/typed-ember/glint): Requires installing the [environment-ember-template-imports](https://github.com/typed-ember/glint/tree/main/packages/environment-ember-template-imports) plugin.
307+
308+
## Editor Integrations
309+
310+
You may need to configure your editor to get syntax highlighting inside embedded templates and support for the `.gjs` and `.gts` file extension.
311+
312+
### Visual Studio Code
313+
314+
The [Ember.js extension pack](https://marketplace.visualstudio.com/items?itemName=EmberTooling.emberjs) bundles everything you need to get started. More specifically, the [vscode-glimmer-syntax](https://marketplace.visualstudio.com/items?itemName=lifeart.vscode-glimmer-syntax) extension will add support for `glimmer-js` and `glimmer-ts` languages and provide syntax highlighting. The [ember-language-server](https://github.com/lifeart/ember-language-server) extension provides automatic import completions and other useful features.
315+
316+
### Neovim
317+
318+
Here's an [example Neovim Config](https://github.com/NullVoxPopuli/dotfiles/blob/main/home/.config/nvim/lua/plugins/syntax.lua#L52) with support for good highlighting of embedded templates in JS and TS, using:
319+
320+
- [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter)
321+
- [tree-sitter-glimmer](https://github.com/alexlafroscia/tree-sitter-glimmer)
322+
323+
### Other editors
324+
325+
For other editors, you may be able to get support using one of these other syntax definitions:
326+
327+
- [TextMate](https://github.com/lifeart/vsc-ember-syntax/tree/master/syntaxes)
328+
- [TreeSitter](https://github.com/alexlafroscia/tree-sitter-glimmer)
329+

guides/release/pages.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@
8686
url: template-lifecycle-dom-and-modifiers
8787
- title: "Built-in Components"
8888
url: "built-in-components"
89+
- title: "Template Tag Format"
90+
url: "template-tag-format"
8991
- title: "Routing"
9092
url: "routing"
9193
pages:

tests/acceptance/side-bar-links-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ module('Acceptance | side bar links', function (hooks) {
2424
setupApplicationTest(hooks);
2525

2626
test('release links go to correct page', async function (assert) {
27-
assert.expect(130);
27+
assert.expect(131);
2828
await visit('/release');
2929

3030
let store = this.owner.lookup('service:store');

0 commit comments

Comments
 (0)