Getting started with Glint and Template Imports
The team I work with are big fans of TypeScript and we've invested a lot of time updating our Ember codebase to be mostly TypeScript now. This has been excellent for maintainability and allowing us to safely add features.
We've got multiple apps we look after and the largest one is over 10 years old. Over the years we invested our time to continuous improvement. The app has extensive tests, visual regression testing and excellent continuous integration pipeline and we deploy frequently.
As we've invested in adding TypeScript to our apps we've seen a reduction in errors that get our into production. This is excellent, but we've still been lacking the ability to use the types in our templates, so there's still a bit of guesswork.
Enter Glint
Glint is a project from the Ember team to allow TypeScript information to be made available to templates and as such, allow us to see type errors in the templates too.
I'll be covering getting started with Glint here and I'll cover using Ember Template Imports (Ember's version of single file components)
I explored Glint about a year ago and found it a little rough going with a lot of effort required to make it work. It wasn't super useful at the time, but it looked promising. The project has recently hit 1.0 and so it was time to take another look.
Installation
Installing was fairly straightforward, following the guide here.
First we add the dependencies to the project yarn add --dev @glint/core @glint/template @glint/environment-ember-loose
.
Then we add some extra information to our existing tsconfig.json
{
"compilerOptions": {
/* ... */
},
"glint": {
"environment": "ember-loose"
}
}
This was the first place I ran into a little challenge, because we also have ember-template-imports
in the stack.
To remedy that issue we need to add another library
yarn add -D @glint/environment-ember-template-imports
.
And update the glint
entry in tsconfig.json
{
"compilerOptions": {
/* ... */
},
"glint": {
"environment": ["ember-loose", "ember-template-imports"]
}
}
We then need to add some imports to the project that make the types available to the language server (more about this later).
They can be added pretty much anywhere, but I chose the existing global.d.ts
file, so they'd have no runtime implications.
import "@glint/environment-ember-loose";
import "@glint/environment-ember-template-imports";
With that done, I can run yarn run glint
and experience the joy of many, many reported errors!
app/manage/subscription/template.hbs:1:1 - error TS7053: Unknown name 'Page::Manage::Subscription'. If this isn't a typo, you may be missing a registry entry for this value; see the Template Registry page in the Glint documentation for more details.
# and hundreds more
This is because currently, no templates have appropriate type information. This is going to be kinda annoying and verbose, but luckily there's a script we can run to annotate all the failing files to be skipped until we're ready to opt in. The script can be found here
With that we want to make it useful in the editor
Making it work in the editor (VS Code)
In order to make Glint work with our editor, we need to add an extension.
With that, we're now able to start making our template type aware.
Here's an example of an error because the template is consuming an undeclared property. Let's see how we can deal with that.
Typing a template
Glint provides us a way to give the compiler information about our components beyond simply typeing the args
.
We can use this interface
interface MyComponentSignature {
Element: HTMLElement;
Args: {
Named: {};
Positional: {};
};
Blocks: {
default: [];
};
}
Element
provides us a place to document what element ...attributes
will be applied to
Args
is where we put things that are to passed to our component, equivalent of this.args
Blocks
refers to {{yield}}
entries in the template.
An example
Let's build a simple buttons component.
import Component from "@glimmer/component";
interface ButtonExampleSignature {
Element: HTMLButtonElement;
Args: {
disabled?: boolean;
type?: "button" | "submit" | "reset";
};
Blocks: {
default: [];
};
}
export class ButtonExample extends Component<ButtonExampleSignature> {
get disabled() {
return this.args.disabled ?? false;
}
get type() {
return this.args.type ?? "button";
}
}
export default ButtonExample;
declare module "@glint/environment-ember-loose/registry" {
export default interface Registry {
ButtonExample: typeof ButtonExample;
}
}
With the associated template
<button
type={{this.type}}
class="rounded border"
disabled={{this.disabled}}
...attributes
>
{{yield}}
</button>
Now consumers of the component know that our ButtonExample
takes two optional properties disabled
and type
. This allows the editor so show us useful information as we try to consume it.
We also know that the HTML Element at the root of our component is a HTMLButtonElement
and that our {{yield}}
doesn't provide anything other than the provided block.
Our editor can also show us this helpful intellisense
information.
We don't yet get total safety, because we can pass an option to @type
that's not on our approved list and glint
doesn't complain.
No red squigglies.
Update after chatting with Dan Freeman on Discord, it turns out this is because the controller for the template I was using to demonstrate this usage was not a TypeScript file, so that may be gotcha for others.
This interface Registry
section is important because it allows templates that are not single file components to lookup components. It's similar to how Models can be looked up using the registry for EmberData based on string keys.
An example with yielded information
This is a contrived example of a ul
element that yields a sorted list, and for our purposes here, we saying we only accept an Array of strings. I'm using single file component syntax for this example.
import Component from '@glimmer/component'
interface UlExampleSignature {
Element: HTMLUListElement
Args: {
items: string[]
}
Blocks: {
default: [items: string[]]
}
}
export class UlExample extends Component<UlExampleSignature> {
get sorted() {
return this.args.items.sort()
}
<template>
<ul ...attributes>
{{yield this.sorted}}
</ul>
</template>
}
export default UlExample
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
UlExample: typeof UlExample
}
}
So consumers of our component know they are expected to provide an argument called @items
which is of type string[]
. We are also able to let them know that the yield
ed item in position 1 will be of type string[]
Here's how that looks in the editor
Example using an imported component
Our UlExample
might be better if we just take a list of items and render them out.
So we'll start with a template only component which represents an <li>
. I've not put this component in the registry, because it only exists to be consumed by the UlExample
. In theory would could also have defined it in the same file as the UlExample
, but this post aims to show the variety of ways you can define things.
import type { TOC } from '@ember/component/template-only'
interface LiSignature {
Element: HTMLLIElement
Blocks: {
default: []
}
}
export const LiExample: TOC<LiSignature> = <template><li ...attributes>{{yield}}</li></template>
export default LiExample
And update our UlExample
to consume it.
import Component from '@glimmer/component'
import LiExample from './li-example'
interface UlExampleSignature {
Element: HTMLUListElement
Args: {
items: string[]
}
Blocks: {
default: []
}
}
export class UlExample extends Component<UlExampleSignature> {
get sorted() {
return this.args.items.sort()
}
<template>
<ul ...attributes>
{{#each this.sorted as |item|}}
<LiExample class="font-bold">{{item}}</LiExample>
{{/each}}
</ul>
</template>
}
export default UlExample
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
UlExample: typeof UlExample
}
}
The Future
I hope you've found this guide helpful.
Glint is looking like a promising addition to the Ember ecosystem and my team is very excited to start benefitting from typed templates. I think this is going to be very helpful for discoverability and error reduction.