Nursing Code

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.

Glint Error

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.

Glint disabled option Glint type option

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.

Glint wrong type option

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 yielded item in position 1 will be of type string[]

Here's how that looks in the editor

Glint yielded

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.