First steps with Glimmer
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.
đ Glimmer can now be used as a standalone library, with incredible performance and payload size! #emberconf
— Lauren Tan â (@sugarpirate_) March 28, 2017
Ember doing it right, easily allowing a component to be used as Web Component https://t.co/GpIWGNGHss
— AndrĂ© Staltz (@andrestaltz) March 28, 2017
Congrats to @emberjs for shipping https://t.co/UGLsbYD1pE. Verified their hello world app is now _30KB_ in production mode. Bring on mobile! pic.twitter.com/rMCJpgLqH0
— Addy Osmani (@addyosmani) March 28, 2017
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
:
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 logicimport 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 betweendeleteComic
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>
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 usingthis.element.querySelector
. - You can acces to components args from template using
@comic
or from component usingthis.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 trackargs
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!!