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:
some markup or
In Octane is now used like this:
<MyComponent />
<MyComponent as |text|>
some markup or
</MyComponent>
Nested components also changed a bit. What would be used as follows:
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
years old.
In Octane now looks like this:
I'm
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
years old.
In Octane, look like this:
I'm
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:
<span>some markup or </span>
<!-- 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
</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
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" >
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= />
Count:
// my-child-component.hbs
<button type="button" >
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=>
Click Me
</button>
<button type="button" onclick=>
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" >
Click Me
</button>
<button type="button" onclick=>
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 ></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:
Should now be used as:
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.