Updating to Ember Octane

Joel Oliveira
Joel Oliveira
Feb 18 2022
Posted in Engineering & Technology

An intro into Ember's first edition release

Updating to Ember Octane

Although there are other, more popular, javascript frameworks out there, Ember.js remains my favorite and the one we use the most at Notificare. There are several reasons why I like it so much:

  • Convention over configuration
  • Built-in testing
  • Clear upgrade paths
  • No company owns it
  • Strong community
  • Add-on ecosystem

On top of this, Ember.js provides everything you need to start developing complex web applications. From a powerful CLI to a built-in Data Layer, these unique features make you productive from day one. There is a reason why we've chose it for our most used frontend application, our dashboard.

In December 2019, Ember.js introduced their first edition release, called Octane (3.15). This new version introduces a programming model that brings major gains in productivity and performance. Through a series of optional, non-breaking releases, new apps can harness modern javascript features, while existing apps can incrementally enable the following new core features of Ember Octane:

Native JavaScript classes

Simpler syntax and better performing code.

Decorators

Customize the behavior of components and other classes.

Tracked properties

A decorator that simplifies DOM updates when properties change.

NPM packages

Import NPM packages with zero additional configuration.

Glimmer components

One of fastest DOM rendering engines out there.

In this post, I will assume you have some knowledge about classic Ember apps and guide you through the changes needed to start using Octane.

Generating Components

In classic Ember, generating a component would generate 3 files, the handlebars template, the javascript file and the test. This would be done using the following Ember CLI command:

ember g component my-component

Which would then add these files:

app/
  components/
    my-component.js
  templates/
    components/
      my-component.hbs
tests
  integration/
    my-component-test.js

In Octane, the javascript class is no longer generated by default. If you want to generate it, you'll need to use the following command:

ember g component my-component -gc

Note the -gc optional parameter which stands for Glimmer Component. This also generates a different structure. In Octane, a component's template and class, live in the components folder:

app/
  components/
    my-component.js
    my-component.hbs
tests
  integration/
    my-component-test.js

Component Templates

Angle brackets are the new way of using components in other templates. What would usually look like this in classic Ember apps:

{{!-- inline --}}
{{my-component}}

{{!-- block --}}
{{#my-component as |text|}}
  some markup or {{text}}
{{/my-component}}

In Octane is now used like this:

{{!-- inline --}}
<MyComponent />

{{!-- block --}}
<MyComponent as |text|>
  some markup or {{text}}
</MyComponent>

Nested components also changed a bit. What would be used as follows:

{{navigation/my-component}}

Is now used as:

<Navigation::MyComponent />

There are also some changes when using named arguments. What usually would look like this in Ember classic:

I'm {{age}} years old.

In Octane now looks like this:

I'm {{@age}} years old.

It is important to note that in Octane these arguments are immutable and can only be modified by the owner class.

Properties owned by the components, that were once used like this:

I'm {{age}} years old.

In Octane, look like this:

I'm {{this.age}} years old.

As you can see, you can now differentiate right away between named arguments and own properties.

It's also important to note that Glimmer components no longer have a wrapper element. In Ember classic, a component used to generate the following code:

{{#my-component as |text|}}
  <span>some markup or {{text}}</span>
{{/my-component}}

<!-- output -->
<div id="ember101" class="ember-view">
  <span>some markup or text</span>
</div>

In Octane, the same code generates the following output:

<MyComponent as |text|>
  some markup or {{text}}
</MyComponent>

<!-- output -->
<span>some markup or text</span>

Component Classes

As mentioned above, Octane introduces Glimmer Components. These use native syntax and have different API methods than classic components. For example what would look like this:

import Component from '@ember/component';

export default Component.extend({
  init() {
    this._super(...arguments);
  }
});

Now looks like this:

import Component from '@glimmer/component';

export default class MyComponent extends Component {
  constructor() {
    super(...arguments);
  }
}

Glimmer components also enforce the "Data Down, Actions Up" paradigm. Because named arguments cannot be changed, the only way to change data is by passing it via an action. In classic components it looked like this:

// my-component.js
import Component from '@ember/component';

export default Component.extend({
  count: 0
});
// my-component.hbs
{{my-child-component count=count}}
Count: {{this.count}}
// my-child-component.js
import Component from '@ember/component';

export default Component.extend({
  actions: {
    addOne() {
      this.set('count', this.get('count') + 1);
    }
  }
});
// my-child-component.hbs
<button type="button" {{action "addOne"}}>
  Click Me
</button>

In Octane, now looks like this:

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

export default class MyComponent extends Component {
  @tracked count = 0;

  @action addOne() {
    this.count++;
  }
}
// my-component.hbs
<MyChildComponent @addOne={{this.addOne}} />
Count: {{this.count}}
// my-child-component.hbs
<button type="button" {{on "click" @addOne}}>
  Click Me
</button>

Arguments in Glimmer components are also easier to distinguish (and impossible to override) from owned properties. What would look like this:

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

export default Component.extend({
  fullName: computed('firstName,lastName', function() {
    return `${this.firstName} ${this.lastName}`;
  }),
});

Now looks like this:

import Component from '@glimmer/component';

export default class MyComponent extends Component {
  get fullName() {
    return `${this.args.firstName} ${this.args.lastName}`;
  }
}

Actions

There is also major changes in the way you handle actions in your components. In classic Ember components, you would use the following:

import Component from '@ember/component';

export default Component.extend({
  actions: {
    log() {
      console.log('Hello, world!');
    },
    logAnotherThing(text) {
      console.log(text);
    }
  }
});
<button type="button" onclick={{action "log"}}>
  Click Me
</button>
<button type="button" onclick={{action "logAnotherThing" "Hallo, world!"}}>
  Click Me
</button>

In javascript native classes, you use the @action decorator to define an action you call from a template, and in the template you use the {{on}} modifier to call a function. Additionally you can use {{fn}} to pass arguments to a function.

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

export default class MyComponent extends Component {
  @action log() {
    console.log('Hello, world!');
  }

  @action logAnotherThing(text) {
    console.log(text);
  }
}
<button type="button" {{on "click" this.log}}>
  Click Me
</button>
<button type="button" onclick={{on "click" (fn this.logAnotherThing "Hallo, world!")}}>
  Click Me
</button>

This results on methods that are left intact and can still be invoked by other methods in your component, instead of being wrapped in the actions object.

Component Lifecycle

Glimmer components also have different lifecycle callback functions. In classic components, you have the init, didInsertElement, didRender, didUpdate and willDestroyElement:

import Component from '@ember/component';

export default Component.extend({
  init() {
    this._super(...arguments);
    // Set properties
  }
  didInsertElement() {
    // Safe to access elements
  }
  willDestroyElement() {
     // Remove event listeners
  }
});

In Glimmer components, you no longer have these. Instead you have a native class that comes with a constructor and the willDestroy callback function:

import Component from '@glimmer/component';

export default class MyComponent extends Component {
  constructor(owner, args) {
    super(owner, args);
  }
  willDestroy() {
    // Remove event listeners
  }
}

Additionally, if you install @ember/render-modifiers, you get the {{did-insert}} and {{did-update}} modifiers. You may use these to mimic the same behavior as before:

<canvas {{did-insert this.initChart}}></canvas>

You should however consider if your functionality really needs these and take the opportunity to rethink if the component functionality can be done in a different way.

Routes

Finally, in a route template, the model data belongs to another context, therefore you should use @model to access this data. Basically, what looked like this:

First name: {{model.first_name}}

Should now be used as:

First name: {{@model.first_name}}

Cool stuff right?

These are the most significant changes in Octane. After almost 10 years of developing on this framework, I think Ember.js continues to mature and stand out from the rest by introducing modern features without completely breaking your apps.

If you have anything to add to this post, feel free to drop us an email. If you are just an Ember.js fan, maybe you might be a great candidate for a Developer position in Notificare. We are currently looking for a Frontend Developer, so feel free to apply to this job opening.

Keep up-to-date with the latest news