First steps with Glimmer

8 minute(s) read

A few weeks ago, during Ember Conf2017, the ember team announced that the rendering engine behind Ember was now available as a standalone library: Glimmer.

This announce has received positive feedbacks not only from the Ember community but also beyond, from the whole JavaScript community.

I’m a great fan of Ember (you can find some good reasons in this article) which really makes me a better and more efficient frontend developper. But the fact is that a great power comes with a great impact on the payload size. The balance is still positive for me but some people and communities have to or choosed to work with micro libraries for many understandable reasons. For those people and communities, the “all in one” approach of Ember is not suitable nor viable.

Glimmer standalone weight is about 30 kB (minified + gzipped). It comes with a really perfomant VM both for initial rendering and for re-rendering (see here). But it also comes with a good part of the Ember ecosystem greatness and offers the possibility to progressively add other parts from any ecosystem.

So, let’s take a deeper look!

The example application

To explore Glimmer basics and capabilities we are going to build, step by step, a simple sample app. When I teach Ember, I use to build a comic books library management application. Here, we are going to build a simpler version of this app. Basically, it is a list of comics and the possibility to select, view, edit and delete each comic. No route, no store, no validation, no persistence. You can find the sources on GitHub.

Starting

Glimmer is a core member of the Ember ecosystem and benefits from its tooling. In particular Ember CLI.

As of today, you must install the canary version (ember-cli-beta.3) to be able to use Glimmer and bootstrap the project:

yarn global add ember-cli/ember-cli

ember new comics-library -b @glimmer/blueprint

Note: If you cannot or do not want to install the canary version of Ember CLI, you can use this instead: ember new comics-library -b https://github.com/glimmerjs/glimmer-blueprint.git

Then, run:

cd comics-library && ember serve

And open http://localhost:4200:

Welcome glimmer

If you take a look at the generated files:

  create .editorconfig
  create README.md
  create config/environment.js
  create config/module-map.d.ts
  create config/resolver-configuration.d.ts
  create ember-cli-build.js
  create .gitignore
  create package.json
  create public/robots.txt
  create src/index.ts
  create src/main.ts
  create src/ui/components/my-glimmer-app/component.ts
  create src/ui/components/my-glimmer-app/template.hbs
  create src/ui/index.html
  create src/ui/styles/app.scss
  create tmp/.metadata_never_index
  create tsconfig.json
  create yarn.lock
Yarn: Installed dependencies
Successfully initialized git.

You’ll notice that the app files can be found inside src/ui directory. Each components inside the components directory is made of:

  • a components.ts file: the TypeScript logic

    import Component from "@glimmer/component";
    
    export default class ComicsLibrary extends Component {
    
    }
    
  • a template.hbs file: the HTML / Handlebars template

    <div>
      <h1>Welcome to Glimmer!</h1>
    </div>
    

Note: This structure is related to the RFC 0143 - Module Unification and should be soon the new Ember convention.

Styles: For an even better experience, you can copy & paste this stylesheet inside your src/ui/styles/app.scss file.

Basic display

Now that we are ready, let’s display a list of comics:

// src/ui/components/comics-library/component.ts

import Component from "@glimmer/component";

export default class ComicsLibrary extends Component {

  comics = [
    {id: 1, title: "Akira", scriptwriter: "Katsuhiro Otomo", illustrator: "Katsuhiro Otomo"},
    {id: 2, title: "BlackSad", scriptwriter: "Juan Diaz Canales", illustrator: "Juanjo Guarnido"},
    {id: 3, title: "Calvin and Hobbes", scriptwriter: "Bill Watterson", illustrator: "Bill Watterson"}
  ];

}
{{!-- src/ui/components/comics-library/template.hbs --}}

<h1>Comic books library</h1>
<div class="comics">
  <h2>Comics list</h2>
  <ul class="comics-list">
    {{#each comics key="@index" as |comic|}}
      <li class="comic-item">
        <a href="#">{{comic.title}} by {{comic.scriptwriter}}</a>
      </li>
    {{else}}
      Sorry, no comic found
    {{/each}}
  </ul>
</div>

Nothing special here since it is almost pure Handlebars syntax. The only difference is the @index key because Glimmer requires us to provide an unique key identifier to iterate on an array.

Actions & Immutability

Let’s add a button to delete a comic on each item of the list:

// src/ui/components/comics-library/component.ts

import Component, {tracked} from "@glimmer/component";

export default class ComicsLibrary extends Component {

  @tracked comics = [
    {id: 1, title: "Akira", scriptwriter: "Katsuhiro Otomo", illustrator: "Katsuhiro Otomo"},
    {id: 2, title: "BlackSad", scriptwriter: "Juan Diaz Canales", illustrator: "Juanjo Guarnido"},
    {id: 3, title: "Calvin and Hobbes", scriptwriter: "Bill Watterson", illustrator: "Bill Watterson"}
  ];

  deleteComic(comic) {
    this.comics = this.comics.filter(x => x.id != comic.id);
  }

}
{{!-- src/ui/components/comics-library/template.hbs --}}

<h1>Comic books library</h1>
<div class="comics">
  <h2>Comics list</h2>
  <ul class="comics-list">
    {{#each comics key="@index" as |comic|}}
      <li class="comic-item">
        <a href="#">{{comic.title}} by {{comic.scriptwriter}}</a>
        <button class="delete-comic-item" onclick={{action deleteComic comic}}>Delete</button>
      </li>
    {{else}}
      Sorry, no comic found
    {{/each}}
  </ul>
</div>

Several things here:

  • actions: as in Ember, DOM events are binded to actions, functions that will handle the event (with an eventual argument)
  • immutability: Glimmer embraces the Immutable Pattern and expects you to replace the state with a new fresh value instead of modifying it. This is why we use the immutable method filter that returns a new array of comics.
  • tracking: Glimmer does not allow to change, after render, a property that has not been marked with the decorator @tracked

See here for additional details.

Containers, Presentational components and DDAU

As Redux before, Glimmer encourages by design the separation of our components into two categories: Containers and Presentational components as defined by Dan Abramov in this post. This design has naturally led to the DDAU principle (Data Down Actions Up) embraced by Redux, Ember, etc. You’ll find a very clear explanation of that here.

Let’s refactor our app to introduce a pure Presentational component to display comic items:

ember g glimmer-component comics-list-item

installing glimmer-component
  create src/ui/components/comics-list-item/component.ts
  create src/ui/components/comics-list-item/template.hbs
{{!-- src/ui/components/comics-library/template.hbs --}}

<h1 >Comic books library</h1>
<div class="comics">
  <h2>Comics list</h2>
  <ul class="comics-list">
    {{#each comics key="@index" as |comic|}}
      <comics-list-item @comic={{comic}}
                        @deleteItem={{action deleteComic}} />
    {{else}}
      Sorry, no comic found
    {{/each}}
  </ul>
</div>
{{!-- src/ui/components/comics-list-item/template.hbs --}}

<li class="comic-item">
  <a href="#">{{@comic.title}} by {{@comic.scriptwriter}}</a>
  <button class="delete-comic-item" onclick={{action @deleteItem @comic}}>Delete</button>
</li>
  • The component comics-list-item is now a pure Presentational component.
  • The component comics-library is the Container, managing the state.
  • We can notice how glimmer is passing arguments to child components using @.
  • Following DDAU principle, actions are defined by the container and given to child components as callback functions.
  • Inside a component Glimmer makes a difference between arguments passed by a parent and invoked with @ and component properties acceeded directly (note the difference between deleteComic in the first component and @deteleItem in the second).

Managing state and tracked properties

Now that we are comfortable, let’s implement comic details and edit pages:

ember g glimmer-component comic-book

installing glimmer-component
  create src/ui/components/comic-book/component.ts
  create src/ui/components/comic-book/template.hbs
// src/ui/components/comics-library/component.ts

import Component, {tracked} from "@glimmer/component";

export default class ComicsLibrary extends Component {

  @tracked current;
  @tracked comics = [
    {id: 1, title: "Akira", scriptwriter: "Katsuhiro Otomo", illustrator: "Katsuhiro Otomo"},
    {id: 2, title: "BlackSad", scriptwriter: "Juan Diaz Canales", illustrator: "Juanjo Guarnido"},
    {id: 3, title: "Calvin and Hobbes", scriptwriter: "Bill Watterson", illustrator: "Bill Watterson"}
  ];

  selectComic(comic) {
    this.current = comic;
  }

  saveComic(comic) {
    let index = this.comics.findIndex(x => x.id === comic.id);
    if (index >= 0) {
      this.comics = [...this.comics.slice(0, index), comic, ...this.comics.slice(index + 1)];
    } else {
      comic.id = this.comics.length + 1;
      this.comics = [...this.comics, comic];
    }
    this.current = comic;
  }

  deleteComic(comic) {
    this.comics = this.comics.filter(x => x.id != comic.id);
    if (this.current && this.current.id === comic.id) {
      this.current = null;
    }
  }

  newComic() {
    this.current = {}
  }
}
{{!-- src/ui/components/comics-library/template.hbs --}}

<h1>Comic books library</h1>
<div class="comics">
  <h2>Comics list</h2>
  <ul class="comics-list">
    {{#each comics key="@index" as |comic|}}
      <comics-list-item @comic={{comic}}
                        @selectItem={{action selectComic}}
                        @deleteItem={{action deleteComic}} />
    {{else}}
      Sorry, no comic found
    {{/each}}
  </ul>
</div>
<button onclick={{action newComic}}>New Comic</button>
{{#if current}}
  <comic-book @comic={{current}} @saveComic={{action saveComic}} />
{{else}}
  <p id="no-selected-comic">
    Please select one comic book for detailled information.
  </p>
{{/if}}
// src/ui/components/comics-list-item/component.ts

import Component from '@glimmer/component';

export default class ComicsListItem extends Component {
};
{{!-- src/ui/components/comics-list-item/template.hbs --}}

<li class="comic-item">
  <a href="#" onclick={{action @selectItem @comic}}>{{@comic.title}} by {{@comic.scriptwriter}}</a>
  <button class="delete-comic-item" onclick={{action @deleteItem @comic}}>Delete</button>
</li>
// src/ui/components/comic-book/component.ts

import Component, { tracked } from '@glimmer/component';

export default class ComicBook extends Component {
  element: Element;

  @tracked editable = false;

  @tracked("args")
  get currentComic() {
    return this.args["comic"];
  }

  @tracked("args", "editable")
  get isEditable() {
    return this.editable || !this.args["comic"].id;
  }

  edit() {
    this.editable = true;
  }

  save() {
    this.editable = false;
    let comic = {
      id: this.args["comic"].id,
      title: this.element.querySelector('#title').value,
      scriptwriter: this.element.querySelector('#scriptwriter').value,
      illustrator: this.element.querySelector('#illustrator').value
    };
    this.args["saveComic"](comic);
  }
};
{{!-- src/ui/components/comic-book/template.hbs --}}

<div class="selected-comic">
  {{#if isEditable}}
    <button onclick={{action save}} class="save-comic">Save</button>
    <input id="title" class="comic-title" type="text" value="{{currentComic.title}}"/>
  {{else}}
    <button onclick={{action edit}} class="edit-comic">Edit</button>
    <h3 class="comic-title">{{currentComic.title}}</h3>
  {{/if}}

  <div class="comic-detail">
    <label>scriptwriter:</label>
    {{#if isEditable}} <input id="scriptwriter" type="text" value="{{currentComic.scriptwriter}}"/>
    {{else}} {{currentComic.scriptwriter}}
    {{/if}}
    <br>
    <label>illustrator:</label>
    {{#if isEditable}} <input id="illustrator" type="text" value="{{currentComic.illustrator}}"/>
    {{else}} {{currentComic.illustrator}}
    {{/if}}
  </div>
</div>

Final app

Here you can notice:

  • The difference between @tracked editable and @tracked('editable'). The first one, as explained before, marks a field that could change after render. The second one decorates a getter (i.e. a property) that will be recomputed each time the observed property is updated. This behaviour must be compared to Ember computed properties.
  • That there is no magic data binding and that you have to copy the values from DOM into objects yourself. You must then declare an Element and get values from DOM using this.element.querySelector.
  • You can acces to components args from template using @comic or from component using this.args['comic'].
  • The state of a Presentational component must be reset whenever we change the selected item by passing arguments. All properties tracking args will then be recomputed automatically. Properties that do not track args directly or via other properties will never be recomputed.

To conclude

Glimmer is the rendering engine of Ember and benefits from the community of its great brother and from amazing tools such as Ember CLI.

But despite this, the standalone Glimmer project is really really fresh and there is still a lot of work to do in terms of documentation, tutorials, improvements, etc. I think that there is also plenty of work on the Ember side, to extract and make available other parts of the framework such as unitary helpers, etc. But I found the approach really pleasant and intuitive. I loved meeting again DDAU principles, Redux way of thinking, immutability, etc. I also really enjoyed TypeScript and was very pleased to take advantage of Glimmer’s tracked annotations to manage computed properties.

In the JavaScript world of today, nobody can predict with any certainty the future of Glimmer as a standalone library but, in my opinion, it is really worth giving it a try!!

Written by

Baptiste Meurant

Software craftsman, developer, architect, unit technical director at Worldline. #java #javascript #emberjs #devops