theme | highlighter | lineNumbers | info | drawings | transition | title | layout | fonts | |||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
apple-basic |
shiki |
false |
## Slidev Starter Template
Presentation slides for developers.
Learn more at [Sli.dev](https://sli.dev)
|
|
slide-left |
Getting Started with <template> Tag Components |
intro |
|
By Ignace Maes
Senior Full-Stack Engineer at OTA Insight
From Ghent, Belgium
@IgnaceMaes
@Ignace_Maes
www.ignacemaes.com
<style> .slidev-layout { position: absolute; height: 100%; width: 100%; } .slidev-layout h1 { font-size: 3rem; line-height: 1; font-weight: 700; text-shadow: 2px 2px #000000; } </style>
::topleft::
- Embroider build system
- First-class TypeScript support
- New Router
- Reactivity
- <template> tag components
::topright::
* We'll get back to this at the end
- Currently Ember uses global string-based resolving
- This has issues:
- Naming conflicts
- No good way to introduce locally-scoped code
- Wider ecosystem tools don't work out of the box as JS context is assumed
- Testing format differs from app code
- Goal: work using references instead
- Extra benefits
- Unlocks code splitting
- Flexible layout for file structure
Problem: How should imports be done in templates?
::topleft::
Text: Hello EmberConf 2023!
::topright::
<CopyToClipboard @text={{"Hello EmberConf 2023!"}} />
application.hbs
copy-to-clipboard.hbs
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
export default class CopyToClipboard extends Component {
@tracked isCopied = false;
copyToClipboard = async () => {
await navigator.clipboard.writeText(this.args.text);
this.isCopied = true;
}
}
copy-to-clipboard.js
::topleft::
#1 Imports only via frontmatter
---
import Icon from 'example-app/components/icon';
import { on } from '@ember/modifier';
---
<button {{on 'click' this.copyToClipboard}}>
{{if this.isCopied 'Copied!' 'Click to copy'}}
<Icon @name={{'clipboard'}} />
</button>
copy-to-clipboard.hbs
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
export default class CopyToClipboard extends Component {
@tracked isCopied = false;
copyToClipboard = async () => {
await navigator.clipboard.writeText(this.args.text);
this.isCopied = true;
}
}
copy-to-clipboard.js
::topright::
#2 Single File Component (Vue/Svelte type)
<script>
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import Icon from 'example-app/components/icon';
import { on } from '@ember/modifier';
export default class CopyToClipboard extends Component {
@tracked isCopied = false;
copyToClipboard = async () => {
await navigator.clipboard.writeText(this.args.text);
this.isCopied = true;
}
}
</script>
<template>
<button {{on 'click' this.copyToClipboard}}>
{{if this.isCopied 'Copied!' 'Click to copy'}}
<Icon @name={{'clipboard'}} />
</button>
</template>
copy-to-clipboard.glimmer
<style> .slidev-layout p { margin-top: 0; } </style>::topleft::
#3 Template literals
import Component, { hbs } from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import Icon from 'example-app/components/icon';
import { on } from '@ember/modifier';
export default class CopyToClipboard extends Component {
@tracked isCopied = false;
copyToClipboard = async () => {
await navigator.clipboard.writeText(this.args.text);
this.isCopied = true;
}
static template = hbs`
<button {{on 'click' this.copyToClipboard}}>
{{if this.isCopied 'Copied!' 'Click to copy'}}
<Icon @name={{'clipboard'}} />
</button>
`
}
copy-to-clipboard.js
::topright::
#4 Template tag component
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import Icon from 'example-app/components/icon';
import { on } from '@ember/modifier';
export default class CopyToClipboard extends Component {
@tracked isCopied = false;
copyToClipboard = async () => {
await navigator.clipboard.writeText(this.args.text);
this.isCopied = true;
}
<template>
<button {{on 'click' this.copyToClipboard}}>
{{if this.isCopied 'Copied!' 'Click to copy'}}
<Icon @name={{'clipboard'}} />
</button>
</template>
}
copy-to-clipboard.gjs
::topleft::
- All four solutions solve the template import problem
- Different trade-offs to be made in
- Semantics
- Learning
- Tooling
- Testing
<template>
tag components came out as best overall- Since RFC #779 the accepted next-gen format
::topright::
See Ember Template Imports blog series by Chris Krycho- Single file for both template and JS/TS
.gjs
and.gts
file extensions- Short for Glimmer JS and Glimmer TS
- Imports required for
- Components
- Helpers
- Modifiers
- Wrapping glimmer template with the
<template>
tag
::topleft::
Before
greeting.hbs
::topright::
After
import Icon from 'example-app/components/icon';
<template>
Hey <Icon @name={{"waving-hand"}} />
</template>
greeting.gjs
::topleft::
Before
copy-to-clipboard.hbs
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
export default class CopyToClipboard extends Component {
@tracked isCopied = false;
copyToClipboard = async () => {
await navigator.clipboard.writeText(this.args.text);
this.isCopied = true;
}
}
copy-to-clipboard.js
::topright::
After
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import Icon from 'example-app/components/icon';
import { on } from '@ember/modifier';
export default class CopyToClipboard extends Component {
@tracked isCopied = false;
copyToClipboard = async () => {
await navigator.clipboard.writeText(this.args.text);
this.isCopied = true;
}
<template>
<button {{on 'click' this.copyToClipboard}}>
{{if this.isCopied 'Copied!' 'Click to copy'}}
<Icon @name={{'clipboard'}} />
</button>
</template>
}
copy-to-clipboard.gjs
<style> .slidev-layout h1 { margin-bottom: 0; } </style>const FEATURE_WORLD = 'some-feature-flag-key';
utils/feature-flags.js
::topleft::
Before
Hello
{{#if (hasFeature this.FEATURE_WORLD)}}
World
{{/if}}
hello-world.hbs
import { FEATURE_WORLD } from 'app/utils/feature-flags';
export default class HelloWorld extends Component {
FEATURE_WORLD = FEATURE_WORLD;
}
hello-world.js
::topright::
After
import hasFeature from 'app/helpers/has-feature';
import { FEATURE_WORLD } from 'app/utils/feature-flags';
<template>
Hello
{{#if (hasFeature FEATURE_WORLD)}}
World
{{/if}}
</template>
hello-world.gjs
const value = 2;
const square = (number) => {
return number * number;
};
<template>
The square of {{value}} equals {{square value}}
</template>
square.gjs
The square of 2 equals 4
const MyListItem = <template>
<div class="p-4 rounded text-white bg-blue-700">
{{yield}}
</div>
</template>;
const MyList = <template>
<div class="p-4 text-bold">List of things</div>
{{#each @items as |item|}}
<MyListItem>
{{item.number}} - {{item.value}}
</MyListItem>
{{/each}}
</template>;
export default MyList;
::topleft::
Before
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { hbs } from 'ember-cli-htmlbars';
module('copy-to-clipboard', function (hooks) {
setupRenderingTest(hooks);
test('renders', async function (assert) {
this.text = 'Hello EmberConf 2023!';
await render(hbs`
<CopyToClipboard @text={{this.text}} />
`);
assert.dom('[data-test-copy]').exists();
});
});
::topright::
After
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import CopyToClipboard from 'components/copy-to-clippy';
module('copy-to-clipboard', function (hooks) {
setupRenderingTest(hooks);
test('renders', async function (assert) {
const text = 'Hello EmberConf 2023!';
await render(
<template>
<CopyToClipboard @text={{text}} />
</template>
);
assert.dom('[data-test-copy]').exists();
});
});
- Template tag format is not opinionated about styling
- Everything "Just Works"
- Separate files for (S)CSS
- Utility class frameworks (e.g. TailwindCSS)
- CSS modules
- Interesting addon developments
glimmer-scoped-css
,embroider-css-modules
,ember-scoped-css
, ...
<template>
<style>
.danger { color: red; }
</style>
<span class="danger">Watch out!</span>
</template>
::topleft::
ember-template-imports
Provides the build tooling required to support Ember's next-gen component authoring format
$ pnpm add --save-dev ember-template-imports
Allows defining .gjs
and .gts
component files
Compatibility
- Ember.js v3.27 or above
- Ember CLI v2.13 or above
- ember-cli-htmlbars 6.0 or above
- Node.js v12 or above
::topright::
See ember-template-imports on GitHubember-template-imports
is an exploration addon to use template tags today
- Content-tag spec for generic language embedding in JS/TS
- Enables future design extensions, e.g.
<gql>
for GraphQL - Framework agnostic: other tools could reuse this format
- Enables future design extensions, e.g.
- New
content-tag
package- Preprocessor for rewriting to valid JS
- Written in Rust on top of Speedy Web Compiler (SWC)
No code impact for end consumer using template tag components!
eslint-plugin-ember
An ESLint plugin that provides a set of rules for Ember applications based on commonly known good practices.
$ pnpm add --save-dev eslint-plugin-ember@^11.6.0
ember-template-lint
ember-template-lint is a library that will lint your handlebars template and return error results.
$ pnpm add --save-dev ember-template-lint@^5.8.0
prettier-plugin-ember-template-tag
A Prettier plugin for formatting Ember template tags in both .gjs and .gts files
Install
$ pnpm add --save-dev prettier-plugin-ember-template-tag
::topleft::
Configure
module.exports = {
plugins: ['prettier-plugin-ember-template-tag'],
overrides: [
{
files: '*.{js,ts,gjs,gts}',
},
],
};
.prettierrc.js
::topright::
VS Code
{
"[glimmer-js]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": false
},
"[glimmer-ts]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": false
}, // and other configs ...
}
settings.json
@glint/environment-ember-template-imports
This package contains the information necessary for glint to typecheck an ember-template-imports project
Install
$ pnpm add --save-dev @glint/environment-ember-template-imports
Configure
::topleft::
{
"extends": "@tsconfig/ember/tsconfig.json",
"glint": {
"environment": [
"ember-loose",
"ember-template-imports"
]
}
}
tsconfig.json
::topright::
import "@glint/environment-ember-loose";
import "@glint/environment-ember-template-imports";
global.d.ts
::topleft::
Visual Studio Code
- vscode-glimmer
chiragpat.vscode-glimmer
- Or Glimmer Templates Syntax for VS Code
lifeart.vscode-glimmer-syntax
Registers glimmer-js
and glimmer-ts
language and grammars
Using another code editor?
Highly likely it's supported, multiple Grammar definitions are available on GitHub
Checkout the ember-template-imports GitHub repository
::topright::
The lifeart.vscode-glimmer-syntax extension on the VS Code Marketplace- Not yet supported
- Currently ~1100
.gjs
/.gts
files on GitHub of the required 2000
- Currently ~1100
- Configurable to fall back to JS/TS syntax highlighting
::topleft::
GitHub
*.gjs linguist-language=js linguist-detectable
*.gts linguist-language=ts linguist-detectable
.gitattributes
::topright::
GitLab
*.gjs gitlab-language=js
*.gts gitlab-language=ts
.gitattributes
- Highlight.js
- Shiki
Interactive tutorial by NullVoxPopuli: https://tutorial.glimdown.com
::topleft::
- Currently not possible to use
.gjs
/.gts
for route templates setRouteComponent
RFC- Unmerged!
- Polyfill available
- The Polaris router will unlock this
::topright::
The Polaris Router roadmap epic on GitHub::topleft::
- Automatic imports in editor for templates
- Ember CLI blueprints
- Documentation
::topright::
The Polaris roadmap on GitHub::topleft::
- Officially the next-gen component format for Ember.js
- Already usable today
- Can be adopted incrementally
- Unlocks code splitting
- Allows access to local scope in templates
- Offers a streamlined testing experience
::topright::
::right::
Ignace Maes
Senior Full-Stack Engineer at OTA Insight
::left::