Nursing Code

Getting started with Glimmer Components in Ember.js


There's a lot of buzz in the Ember community about the upcoming release of Ember Octane Edition.

It represents an enormous amount of work by the Ember core team and community.

The release will roll out a hugely simplified javascript model and the use of Glimmer components radically alters the way we need to write code.

I'll be using TypeScript for many of the code examples.

The source code is available on Github

A good place to play with Octane is via CodeSandbox using @nullvoxpopuli's starter kit

What's the difference between a 'Classic' Component and a Glimmer Component?

First major difference is that you use a different import

import Component from '@glimmer/component';

vs

import Component from '@ember/component';

The next thing that is very different is that you no longer have a 'wrapping' element provided.

With a Classic component, you always have a wrapping div (unless you set tagName: ''). With a Glimmer component, you only get what you explicitly set in your template.

You also get access to something referred to as splattributes. Splattributes will take any attributes you supply to the component invocation and add them where you call ...attributes. For more information take a look at the RFC

Given a template like this

<img ...attributes>
</img>

and an invocation like this

<SafeImage src="https://www.example.com/image1.png">

Then end result would be

<img src="https://www.example.com/image1.png">

We'll look at more sophisticated usage of this as we go on.

The next major difference is the ability to use @tracked. This decorator is like magic.

It allows us to 'just' use properties and assign value to them. No more this.get and this.set.

For example

import Component from '@glimmer/component';
import {tracked} from '@glimmer/tracking';
import { action } from '@ember/object';

export default class Counter extends Component {
  @tracked count: number = 0;

  get formattedCount() {
    return this.count.toString().padStart(3, '0');
  }

  @action
  increment() {
    this.count++
  }

  @action
  decrement() {
    this.count--
  }
}
<span>{{this.formattedCount}}</span>
<button {{on "click" this.increment}}>Increment</button>
<button {{on "click" this.decrement}}>Decrement</button>

Clicking either button will mutate the count and the formattedCount will automatically be updated and the DOM will reflect that change.

The 'on' Modifier

Where you may have done these in the past

<button {{action 'someAction'}}></button>
<button onclick={{action 'someAction'}}></button>

The recommended way going forward is the on modifier. It is used like this

<button {{on 'click' this.doStuff}}></button>

With a backing class such as

import Component from '@glimmer/component';

export default class Counter extends Component {
  doStuff(event: MouseEvent) {
    // We do not have access to `this`
  }
}

doStuff will be invoked when the button is clicked and will have the event as the first argument.

It's important to note that with the code about, we do not have access to the actual component as this.

If we want to access this, we need to use the action decorator

import Component from '@glimmer/component';
import { action } from '@ember/object';

export default class Counter extends Component {
  @action
  doStuff(event: MouseEvent) {
    // We now have access to `this`
  }
}

Expanding on a SafeImage Component

With this new knowledge, we can easily build a component that can deal with issues where an image may fail to load or may even not be present. In that case, maybe we want to show some kind of placeholder image.

<img
  ...attributes
  {{on "error" this.fallbackImage }}
>

So we apply the ...attributes and register a handler for onerror

import Component from '@glimmer/component';

interface SafeImageArgs {}

export default class SafeImage extends Component<SafeImageArgs> {
  fallbackImage(event: ErrorEvent) {
    const target = event.target as HTMLImageElement;

    target.src = "https://picsum.photos/200/300";
  }
}

In the event that an error occurs, we will now load the placeholder image.

What's really neat is that with the ...attributes we can pass any attributes to the image component we like.

So

<SafeImage src="https://example.com/foo.png"
   alt="Avatar image"
   class="avatar"
   height="50"
   width="50"
/>

would result in

<img 
  src="https://picsum.photos/200/300" 
  alt="Avatar image" 
  class="avatar" 
  height="50" 
  width="50">

This makes for super flexible components and gets rid of the need to be super verbose with boundAttributes = ['alt', 'src', 'height', 'width']!

Class attribute is a bit special

One thing that is worth nothing is that if you already have a class attribute in your template, the passed values will be merged.

<img class="AlwaysApplied" ...attributes>
<SafeImage src="https://example.com/foo.png"
   alt="Avatar image"
   class="avatar"
   height="50"
   width="50"
/>

Would result in

<img 
  src="https://picsum.photos/200/300" 
  alt="Avatar image" 
  class="AlwaysApplied avatar" 
  height="50" 
  width="50">

Note that AlwaysApplied is merged with the passed class.

Arguments and mutability

We haven't yet looked at passing values to our components to be used in the JS or the template.

Glimmer components, unlike classic components do not merge all the passed arguments with the base class, instead they are collected on a namespace args and must be passed with the @ prefix.

When accessing this.args items are immutable (mostly, sort of ...)

Given a Mutator component we are going to pass two items, a string and an object.

// within class body
plainValue =  'Hello';
mutableObject = {
  message: 'Goodbye'
}
array = [];
<Mutator
  @plainValue={{this.plainValue}}
  @mutableObject={{this.mutableObject}}
  @array={{this.array}}
/>

The backing template looks like this.

<button {{on "click" this.tryImmutable}}>
  Cannot mutate
  {{@plainValue}}
</button>

<button {{on "click" this.tryMutable}}>
  Can mutate interior ->
  {{@mutableObject.message}}
</button>

<button {{on "click" this.pushToArray}}>
  Push to array {{#each @array as |item|}}{{item}}, {{/each}}
</button>

and the JS / TS looks like this

import Component from '@glimmer/component';
import { action, set } from '@ember/object';

interface MutatorArgs {
  plainValue: string;
  mutableObject: {
    message: string;
  };
  array: any[]
}

export default class Mutator extends Component<MutatorArgs> {
  @action
  tryImmutable() {
    set(this.args, 'plainValue', 'Goodbye');
  }

  @action
  tryMutable() {
    set(this.args.mutableObject, 'message', 'Goodbye');
  }

  @action
  pushArray()  {
    this.args.array.pushObject('Goodbye');
  }
}

What's that funky interface stuff about?

if we hit the Cannot Mutate button, we trigger the tryImmutable method which is going to try and reassign plainValue on this.args. We will see an error that looks like this in our console

You attempted to set #plainValue on a components arguments. Component arguments are immutable and cannot be updated directly, they always represent the values that are passed to your component. If you want to set default values, you should use a getter instead

Basically, you are not allowed to reassign a value on args.

If we are, however dealing with an object (or an array, which of course in JS is just an object), then we do have the ability to make changes.

Hitting Can mutate interior -> Hello will change it to Can mutate interior -> Goodbye, whilst hitting Push to array will start adding the string 'Goodbye'. Note that I used pushObject, because using push would update the contents of the array, but it would not trigger updates of the DOM as Ember would not know to recompute.

Just because you can doesn't mean you should

Now that I've shared how it's possible to 'overcome' the immutability of args, I want to say that you should really think about whether that is the right thing to do.

Like many scenarios in software development, just because you can, doesn't mean you should. In most scenarios it's better to pass down an action and use that to update the value

One area I think it's fairly reasonable to do so is to two way bind an <input>, though you don't need to jump through so many hoops there.

With that scenario, you may do this

<MyForm @firstName={{this.data.firstName}}>

And in your form template

<Input @value={{@firstName}}>

And you now have two way binding. This is still a bit of a contentious issue in the community, so I'm sharing information only, not judging you!

Interfaces

One super nice feature of Typescript and Glimmer components is that you can add types via an interface.

From our example earlier.

interface MutatorArgs {
  plainValue: string;
  mutableObject: {
    message: string;
  };
  array: any[]
}

export default class Mutator extends Component<MutatorArgs>

Now if we try do something funky like this.plainValue.toExponential(10), our compiler can yell at us because a string doesn't support that method.