Updated 2019-09-07: updated to reference that the Ember beta is used.

Ember has been undergoing a lot of development recently to add features to make it easier to understand and allow it to take advantage of emerging JS ecosystem conventions. This includes:

  • Angle bracket syntax for components
  • Easy ES6 imports of NPM modules
  • Decorators for clear and expressive component implementations
  • Tracked properties to automatically rerender components and recompute derived data
  • Co-locating component templates and classes for easy development

Some of these features are already available in the stable release of Ember, and others will land as part of the upcoming Ember Octane edition. But we can try them all today as part of the Octane preview! Let’s look at what it’s like to develop in Ember with all of these modern features in place. You can also download the completed project if you like.

Project Setup

To get the latest Ember features, we can use ember-cli to create a project using the Octane blueprint. First, make sure ember-cli is installed:

$ npm install -g ember-cli

Then, create a new project with the Octane blueprint:

$ ember new modern-ember -b @ember/octane-app-blueprint

It’s important to be aware that this will install a beta build of Ember. Octane is feature-complete and moving toward a stable release, but it would still be a good idea to be cautious when deciding whether to use the Octane Preview for a production application.

The installation process will take a few minutes to run. When it finishes, we’re all set!

Components

With Ember you can jump right in to creating components without needing to look into other concepts yet. ember-cli will allow us to easily generate a component’s files:

$ ember generate component MyComponent

This will create the following files:

  • app/components/my-component.hbs - the markup for the component
  • tests/integation/components/my-component-test.js - an automated test. Ember automatically adds tests alongside files it generates, but we won’t be looking into testing as part of this tutorial.

Note that the component file is a Handlebars file, which is just for output. If we need to add logic to the component, it will go in a separate JavaScript file, but this file isn’t generated by default. Having separate files has the advantage that each file is a normal .js or .hbs file.

Let’s add a message to the component markup. Replace the contents of my-component.hbs with:

Hello!

Now we want to display the component. The main template file that renders the app is app/templates/application.hbs. We can just add the component in there. Delete the contents of the file and replace it with:

<MyComponent />

Note that components don’t need to be imported anywhere; they’re automatically available in templates.

We can see this working by starting up the server with ember serve. Go to http://localhost:4200 and you should see the “Hello!” message. You can leave the server running.

Arguments and Properties

Next, let’s pass in an argument to our component. Arguments are passed with an @ prefix:

<MyComponent @name="world" />

(Arguments are equivalent to props in React or Vue.)

In the component template itself, we access that argument using the @ sigil as well. This makes it clear at the point of use that the data comes in as an argument:

Hello, {{@name}}!

So far, this component is a stateless template-only component. The only data it has access to is what’s passed into it.

If we want the component to store its own data as well, we can add a JavaScript backing class for the component. This will allow us to store properties on the component itself and display them. (Properties are equivalent to state in React or data in Vue. They’re called “properties” because they’re just normal JavaScript object properties!)

Create a file app/components/my-component.js and add the following:

import Component from '@glimmer/component';

export default class MyComponent extends Component {
  count = 0;
}

Ember uses plain object properties for data storage, rather than a framework-specific mechanism like a state or data object.

You can reference this property in the template with a this. prefix, just as you would in JavaScript:

Hello, {{@name}}!

{{this.count}}

Actions

To update the data, we can implement an action on the component. We use a decorator to indicate that a method is available as an action.

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

 export default class MyComponent extends Component {
-  count = 0;
+  @tracked count = 0;
+
+  @action
+  increment() {
+    this.count += 1;
+  }
 }

By tagging a property with the @tracked decorator, we indicate to Ember that the component should be rerendered when it is changed–for example, when we increment it by 1 here.

To call this action, add a button to the template:

 Hello, {{@name}}!

 {{this.count}}
+
+<button {{on "click" this.increment}}>Increment</button>

Computed Properties

Components can also have computed properties, implemented as normal ES5 getters. These are cached, and will be automatically recomputed when the @tracked properties they depend upon change.

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

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

+  get uppercaseName() {
+    return this.args.name.toUpperCase();
+  }
+
   @action
   increment() {

Note that in the component the @name argument is available as this.args.name. Arguments aren’t placed directly on this to ensure that they won’t collide with properties or actions on the component instance itself.

Then we update our template to pull from the computed property. Just like other properties, computed properties are accessed with the this. prefix to indicate that it’s a property of the component instance.

-Hello, {{@name}}!
+Hello, {{this.uppercaseName}}!

Using NPM Packages

In Ember you can load data the same way you would in any other framework. To see this in action, let’s import the popular Axios web service client library:

$ npm install --save axios

Note that you don’t need to stop and rerun ember serve to get the package loaded; it’ll pick it up automatically.

Next, in app/components/my-component.js, initialize a records property and populate it in the constructor() - this is the normal ES6 class constructor that runs when the instance is initialized. We’ll use JSONPlaceholder for some convenient JSON data:

 import { action } from '@ember/object';
+import axios from 'axios';

 export default class MyComponent extends Component {
   @tracked count = 0;
+  @tracked records = [];
+
+  constructor(owner, args) {
+    super(owner, args);
+
+    axios
+      .get('https://jsonplaceholder.typicode.com/posts')
+      .then(response => {
+        this.records = response.data;
+      });
+  }

   get uppercaseName() {

Display it in the template:

 <button {{on "click" this.increment}}>Increment</button>

+{{#each this.records as |record|}}
+  <p>
+    {{record.title}}
+  </p>
+{{/each}}

#each is a helper that loops through an array, and provides a variable to the nested markup.

Moving Content to a Route Template

Now, say we want to display the contents of a post. Usually users would expect this to be on a separate route. Ember includes routing out of the box.

Earlier we added <MyComponent /> to our application/template.hbs file. This file is the template that is always displayed no matter which route your app is on. To get MyComponent to display only on the “home page”, generate an index route:

$ ember generate route index

This should create the following files

  • app/routes/index.js - the route class
  • app/templates/index.hbs - the route markup
  • a test file

Copy the component invocation from app/templates/application.hbs and paste it into index.hbs:

<MyComponent @name="world"/>

Now replace the contents of app/templates/application.hbs with the following:

<h1>My App!</h1>

{{outlet}}

outlet is a helper that will output the contents of the specific route you’re on. Reload your app and you should see the “My App” header as well as your MyComponent content.

Creating Another Route

Let’s create a route for individual posts, using Ember CLI again. As you can tell, in Ember we use ember generate a lot; you can use ember g as a shortcut:

$ ember g route post

The generate route command modified the file app/router.js. Check the changes:

Router.map(function() {
  this.route('post');
});

This configures a route at /post. But we want to modify it to be at /post/27 for post ID 27, for example. To do this, we can customize the path of this route to include a dynamic segment, indicated with a : prefix:

Router.map(function() {
  this.route('post', { path: 'post/:id' });
});

You can’t access the id variable from just anywhere; the place that has access to it is a route class, app/routes/post.js. Open it. A route’s dynamic segments are passed to a method called model() if one is defined. Add it:

 export default class PostRoute extends Route {
+  model({ id }) {
+  }
 }

Note that we can use destructuring to get the id.

For now, let’s generate some fake post data. We’ll come back to share data between the routes later:

export default class PostRoute extends Route {
   model({ id }) {
+    return {
+      title: `Post ${id}`,
+      body: `This is post ${id}!`,
+    };
   }
}

In the route’s template app/templates/post.hbs, let’s render a PostDetail component:

<PostDetail @post={{this.model}} />

Earlier we used quotes around an argument when passing a hard-coded string, like you would for HTML attributes. When we want to pass any other JavaScript type or a dynamic value (such as the object stored in this.model here) we need to use double-curlies.

Generate this PostDetail component:

$ ember g component PostDetail

Then fill in its template:

<h3>{{@post.title}}</h3>

<p>{{@post.body}}</p>

This component is stateless so we won’t need a backing JavaScript class. Nice and simple!

Now, back in my-component.hbs, we can link our post titles to that route:

{{#each this.records as |record|}}
   <p>
-    {{record.title}}
+    <LinkTo @route="post" @model={{record.id}}>
+      {{record.title}}
+    </LinkTo>
   </p>
 {{/each}}

LinkTo is a built-in component that sets up a link to another route. We can pass a separate argument to it and it’s filled in as the dynamic segment.

The links should be clickable and take you to the route with the right ID, and our fake data will be shown.

Loading Data in a Service

To share data between components, we can put it in a service. The service can be injected into any component that needs it.

Generate a new service:

$ ember g service posts

This creates the following files:

  • app/services/posts.js - the service class
  • a test file

Add a getAll() method to the service that implements loading the data:

 import Service from '@ember/service';
+import axios from 'axios';

 export default class PostsService extends Service {
+  async getAll() {
+    const response = await axios.get(
+      'https://jsonplaceholder.typicode.com/posts'
+    );
+    return response.data;
+  }
}

Note that we can use async/await - it’s enabled by default in Ember’s Babel config.

Now update MyComponent to inject the Posts service to get the data from there instead of using Axios directly:

 import { action } from '@ember/object';
-import axios from 'axios';
+import { inject as service } from '@ember/service';

 export default class MyComponent extends Component {
+  @service posts;
+
   @tracked count = 0;
   @tracked records = [];

   constructor(owner, args) {
     super(owner, args);

-    axios
-      .get('https://jsonplaceholder.typicode.com/posts')
-      .then(response => {
-        this.records = response.data;
-      });
+    this.posts.getAll().then(posts => {
+      this.records = posts;
+    });
   }

   get uppercaseName() {

Note that just by using the @service decorator on a property, Ember knows to inject the Posts service when it creates the component. There’s no multi-step provider component process. This “dependency injection” approach also makes testing easier, because in a component test you can inject a fake service instead of the real one.

Service State

To avoid having to re-retrieve the post when we go to the detail page, let’s cache the post data in the service.

 export default class PostsService extends Service {
+  posts = null;
+
   async getAll() {
-    const response = await axios.get(
-      'https://jsonplaceholder.typicode.com/posts'
-    );
-    return response.data
+    if (this.posts === null) {
+      const response = await axios.get(
+        'https://jsonplaceholder.typicode.com/posts'
+      );
+      this.posts = response.data;
+    }
+
+    return this.posts;
   }
 }

Now that the posts are cached, we can easily add a method to retrieve a single post by ID:

     return this.posts;
   }
+
+  async findById(postId) {
+    const posts = await this.getAll();
+    return posts.find(p => p.id === postId);
+  }
 }

Note that we call this.getAll() instead of accessing this.posts directly. This way, even if getAll() hasn’t been called before, we’ll be sure to retrieve them.

Inject the service into the post route and call it in the model() method to return the data:

 import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';

 export default class PostRoute extends Route {
+  @service posts;

   model({ id }) {
-    return {
-      title: `Post ${post_id}`,
-      body: `This is post ${post_id}!`,
-    };
+    return this.posts.findById(Number(id));
   }
 }

Now, go to the root of your app, click a blog post link, and you should be taken to a detail page with the data.

Conclusion

We’ve looked at a lot of Ember’s new features: angle bracket syntax, template co-location, ES6 classes and decorators, tracked properties, auto importing of NPM modules. These changes remove some of the incidental differences between Ember and other frameworks, as well as some legacy cruft from before components were the mental model and before ES6 classes existed. Put together, they give Ember a very modern feel.

If you’re interested in a framework that gives you a reliable upgrade path as the web evolves, that takes care of implementation details so you can focus on your application’s business logic, and that values creating addons so you don’t have to reinvent the wheel, there are a lot of great reasons to check out Ember.