Getting Started with Ember Octane: Building a Blog

Last reviewed on June 16, 2019 with Ember Octane

Ember is a great fit for all kinds of front-end apps, but it really shines with the growing complexity of medium to large-sized projects.

In order to learn it we are going to start small. We will build a simple blog app with basic CRUD functions:

Blog index with input to create new post
Blog index with input to create new post
Post page with remove button
Post page with remove button

In the process we’ll cover a bunch of useful things:

  • Using Ember Data to load data, save & delete
  • Installing add-ons
  • Setting up Ember Mirage as a backend
  • Using actions
  • Understanding the infamous “Ember Magic”!
  • Creating a nested routing structure
  • Defining tracked properties
  • Using components & helpers
  • Redirecting
  • Seeing how all the pieces work together with diagrams
This tutorial will cover essential Ember concepts. If you want to understand them in detail, I recommend reading 5 Essential Ember Concepts You Must Understand.

Ember’s philosophy is reflected in its mottos “Built for productivity”, “Don’t reinvent the wheel” and “Don’t waste time making trivial choices”. This tutorial will give us a glimpse into some of the best practices in the Ember ecosystem.

Let’s get started!

1. Getting the tools ready

Ember CLI is the command-line interface for creating and maintaining Ember apps. It’s essential for a productive development experience and, as such, the starting point of any new app. It facilitates and enforces common idioms.

Based on an asset pipeline this tool brings a lot to the table:

But most importantly, it enforces a strong conventional project structure (because, remember, “Don’t waste time making trivial choices”).

To install:

$ npm install -g ember-cli

Done?

Let’s make sure the ember command is ready to use:

$ ember -v
ember-cli: 3.10.1
node: 8.11.1
os: darwin x64
Need to update Ember CLI or an Ember project? Here’s how!

Now let’s create the blog project:

$ ember new my-blog -b @ember/octane-app-blueprint

(We need to specify the Octane blueprint until the edition is officially released.)

2. Adding a route

We’ll create the first route of our app, the PostsRoute:

$ cd my-blog
$ ember generate route posts
installing route
  create app/routes/posts.js
  create app/templates/posts.hbs
updating router
  add route posts
installing route-test
  create tests/unit/routes/posts-test.js

Now let’s try to break down what ember generate route posts just did:

(a) It created a route file

// app/routes/posts.js

import Route from '@ember/routing/route';

export default class PostsRoute extends Route {
  model(params) {
    return this.store.findAll('post');
  }
}

(Don’t add those lines to the file just yet.)

The main hook in routes is model() from where we retrieve our primary model.

Ember Magic! You won’t see the highlighted lines in the recently created file but that is exactly what Ember auto-generates by default when no model() is supplied.

If the URL was /posts/1 instead (a parameterized route of a single resource) the auto-generated model would look like:

// app/routes/posts.js

import Route from '@ember/routing/route';

export default class PostsPostRoute extends Route {
  model({ post_id }) {
    return this.store.findRecord('post', post_id);
  }
}

We’ll get later to the meaning of store and where that comes from.

Keep in mind that:

model() is sometimes referred to as “the model() hook” because it’s a specific method invoked by the Ember framework every time it needs to handle a request.

model() can either return an object (synchronous) or a Promise (asynchronous). If it’s a Promise, the route will halt until the Promise has resolved before moving forward with the rendering phase.

(b) It created the corresponding template

The template is where we’ll place our markup. The model property is available here.

{{! app/templates/posts.hbs }}

{{outlet}}

The {{outlet}} helper is used to include the contents of any sub-routes of the route we’re on! If the route has no nested routes (like PostsRoute right now) it is safe to remove it.

Ember Magic! Remember that whenever there is a template there is a controller because they essentially are two sides of the same coin. Ember does not generate a controller file; however it auto-generates an in-memory PostsController when no file is present.

(c) It added a routing definition in the router

// app/router.js

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

This means that the /posts URL will be handled by the PostsRoute. In Ember everything starts at the URL.

(d) It created a unit test file

We’ll get to the fantastic testing infrastructure in another article!

3. Listing posts

Open the my-blog project in your favorite editor, and boot Ember up:

$ ember serve
Build successful (3046ms) – Serving on http://localhost:4200/

Visiting localhost:4200 will show a welcome message!

diagram

Get rid of this message by removing the <WelcomePage /> component in the application template and add a title. This is how it should end up looking:

{{! app/templates/application.hbs }}

<h2 id="title">
  <a href="/posts">My Ember blog</a>
</h2>

{{outlet}}

Return an actual model in the route

Let’s return data for our template to use.

// app/routes/posts.js

import Route from '@ember/routing/route';

export default class PostsRoute extends Route {
  model() {
    return this.store.findAll('post');
  }
}

The default way to interact with a backend API is through Ember Data (findAll, query, save, etc).

Ember Data is accessible via its store API, which is a Service injected into routes during the app boot phase. It is therefore available to all routes in an Ember app.

An Ember Service is a long-lived singleton object that can be accessed throughout the application. It can often hold state and it’s typically used for cross-cutting concerns such as logging, data management and authentication.

Ember Data and Ember Simple Auth are the most well-known services in the Ember world.

Ember Octane best practices straight to your inbox? Join other 3800+ smart developers!

Back to our blog! If we are requesting Ember Data to retrieve data – which backend is it supposed to retrieve it from?

A quick backend

We are going to introduce an awesome mocking library called Ember Mirage. Insanely useful for development and testing.

$ ember install ember-fetch  # required
$ ember install ember-cli-mirage
installing ember-cli-mirage
  create /mirage/config.js
  create /mirage/scenarios/default.js
  create /mirage/serializers/application.js
Installed addon package.

And now Ember lets us import any npm package directly! We’ll use Faker:

$ npm install --save-dev faker

Now that we’re all set up, we’ll generate a model and a factory for our posts.

$ ember g mirage-model post
$ ember g mirage-factory post
// mirage/factories/post.js

import { Factory } from 'ember-cli-mirage';
import faker from 'faker';

export default Factory.extend({

  title() {
    return faker.lorem.sentence();
  },

  body() {
    return faker.lorem.paragraph();
  },

  publishedAt() {
    return faker.date.past();
  }

});

Next, we will leave our configuration file like this:

// mirage/config.js

export default function() {
  this.resource('posts');
}

Defining a resource gives us all CRUD operations for posts.

To seed the mock server, let’s generate some posts:

// mirage/scenarios/default.js

export default function(server) {
  server.createList('post', 5);
}

It is important to keep in mind that both Ember Mirage and Ember Data use the JSON:API specification by default. This is why we didn’t have to customize or configure anything related to data communication protocols.

If you are interested, here is an entire article about configuring any API endpoint to work with Ember Data.

Okay we now have a Post model in our backend, but not in Ember! Let’s generate it:

$ ember g model post
installing model
  create app/models/post.js
installing model-test
  create tests/unit/models/post-test.js

We’ll now declare our attributes title, body and publishedAt. Attribute types will default to string.

// app/models/post.js

import DS from 'ember-data';
const { Model, attr } = DS;

export default class PostModel extends Model {
  @attr() title;
  @attr() body;
  @attr('date') publishedAt;
}

Lastly, we only have to tell our template to loop through this.model (an array of posts):

{{! app/templates/posts.hbs }}

<ul>
  {{#each this.model as |post|}}
    <li>
      {{post.title}}
    </li>
  {{/each}}
</ul>

{{outlet}}

This can be a bit confusing. How did we get from the route to Ember Data to the template?

Here is an overview of our application’s data flow that hopefully clears it up:

Notice the uni-directional data flow. Data flows down (solid lines).
Notice the uni-directional data flow. Data flows down (solid lines).

So now let’s run ember serve (or ember s for short) again and load up http://localhost:4200/posts:

diagram

Success?

Make sure that if you are visiting /posts! Otherwise visiting / the PostsRoute will never be called, so you will only see “My Ember blog”!

4. Creating new posts

Let’s add an input to add post titles:

{{! app/templates/posts.hbs }}

<Input @value={{this.newTitle}} @enter={{action this.addPost}} />

<ul>
  {{#each this.model as |post|}}
    <li>
      {{post.title}}
    </li>
  {{/each}}
</ul>

{{outlet}}

Okay, what’s going on?

We are using the Ember Input built-in component.

  • this.newTitle: the property on the controller to bind to the newly input title
  • this.addPost: the action that will be called when pressing the enter key

You guessed right, we need a controller to place that action!

$ ember generate controller posts
installing controller
  create app/controllers/posts.js
installing controller-test
  create tests/unit/controllers/posts-test.js

Now we open the file and type in the addPost action.

import Controller from '@ember/controller';
import { action } from '@ember/object';

export default class PostsController extends Controller {

  newTitle;

  @action
  addPost() {
    this.store.createRecord('post', {
      title: this.newTitle,
      publishedAt: new Date()
    }).save();
    this.set('newTitle', "");
  }
}

It creates an Ember Data model with the current value of the newTitle property in the controller and then saves it to the backend. While also cleaning the user input. Neat!

diagram

Do you need professional help with Ember or your back-end?

Leave me your name and e-mail and I will get in touch shortly:
You can also    Book session on Codementor

5. Post details

We want to view post details (including the contents of the post) with a URL like http://localhost:4200/posts/1.

For that we’ll create a post route:

$ ember generate route posts/post
installing route
  create app/routes/posts/post.js
  create app/templates/posts/post.hbs
updating router
  add route posts/post
installing route-test
  create tests/unit/routes/posts/post-test.js

Ember has created a nested route post inside of posts (named PostsPostRoute). Let’s have a look at the router:

// app/router.js

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

In order to parameterize the URL we need to use the path property:

// app/router.js

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

Since we want to reuse markup of the PostsRoute in PostsPostRoute we are going to create a component.

$ ember generate component post-view
installing component
  create app/components/post-view.js
  create app/templates/components/post-view.hbs
installing component-test
  create tests/integration/components/post-view-test.js

We want it to nicely display the title, date and conditionally the body. It should be invoked like:

<PostView @post={{post}} @showBody={{true}} />

Here is a possible implementation:

{{! app/templates/components/post-view.hbs }}

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

{{#if @showBody}}
  <span>{{@post.publishedAt}}</span>
  <pre style="white-space: pre-wrap;">{{@post.body}}</pre>
{{/if}}

How would our post template look when we call this component?

{{! app/templates/posts/post.hbs }}

<PostView @post={{this.model}} @showBody={{true}} />

Notice how we are passing in different arguments in the posts page. We are also wrapping the component in a link to the post detail page.

{{! app/templates/posts.hbs }}

<Input @value={{this.newTitle}} @enter={{action this.addPost}} />

<ul>
  {{#each this.model as |post|}}
    <li>
      <LinkTo @route="posts.post" @model={{post.id}}>
        <PostView @post={{post}} @showBody={{false}} />
      </LinkTo>
    </li>
  {{/each}}
</ul>

{{outlet}}

Confused about when to use this or @?

  • {{@post}} refers to an argument passed to the component (as you can see on the example above)
  • {{this.post}} would refer to a post property on the component class (for example this.model)
  • {{post}} would refer to a helper with no arguments (except in the case of the post local variable in the loop above)

Reloading /posts in the browser:

diagram

Clicking on the first item should load /posts/1

diagram

It works– but there’s something off. Why is the listing of posts included at the top?! We just want to see title, publishing date and body of the post.

Route nesting

Let’s rewind for a second and have a look at the posts template:

{{! app/templates/posts.hbs }}

<Input @value={{this.newTitle}} @enter={{action this.addPost}} />

<ul>
  {{#each this.model as |post|}}
    <li>
      <LinkTo @route="posts.post" @model={{post.id}}>
        <PostView @post={{post}} @showBody={{false}} />
      </LinkTo>
    </li>
  {{/each}}
</ul>

{{outlet}}

We saw earlier that {{outlet}} is used to place the contents of nested routes (sub-routes). And yes, PostsPostRoute (post, the detail page) is definitely a sub-route of PostsRoute (posts, the listing page).

Ember is behaving exactly as it’s told.

`routes/posts.js` is a parent of `routes/posts/post.js`
routes/posts.js is a parent of routes/posts/post.js

So, how do we get the URL /posts to be a sibling (not a parent) of /posts/1?

Ember Magic! For every trailing slash in the whole hierarchy, at every single leaf, there is an implicit index route.

// app/router.js

Router.map(function() {
  this.route('posts', function() {
    this.route('index'); // no need to define it here, it's implicit
    this.route('post');
  });
});

If we use the PostsIndexRoute instead of PostsRoute we keep the same URL structure while using PostsRoute’s {{outlet}} to output either PostsIndexRoute or PostsPostRoute.

diagram

Makes sense?

So let’s go ahead and make those changes. You can use Bash or manually move files around in your editor.

# rename route
$ mv app/routes/posts.js app/routes/posts/index.js

# rename template
$ mv app/templates/posts.hbs app/templates/posts/index.hbs

# rename controller
$ mkdir app/controllers/posts
$ mv app/controllers/posts.js app/controllers/posts/index.js

Important: Make sure app/routes/posts/index.js exports the class PostsIndexRoute and app/controllers/posts/index.js the class PostsIndexController.

diagram

Problem solved!

6. Deleting posts and redirecting

With a simple button we will give the user the possiblity of deleting a post.

{{! app/templates/components/post-view.hbs }}

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

{{#if @showBody}}
  <span>{{@post.publishedAt}}</span>
  <pre style="white-space: pre-wrap;">{{@post.body}}</pre>

  <button {{on "click" (action this.removePost @post)}}>
    Remove post
  </button>
{{/if}}

(on is a modifier in action.)

Naturally, we have to implement this.removePost:

// app/components/post-view.js

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

export default class PostViewComponent extends Component {

  @service router;

  @action
  async removePost(post) {
    await post.destroyRecord();
    this.router.transitionTo('/posts');
  }
}

A few interesting things in this last snippet. The removePost async function first destroys the supplied post record (through Ember Data) and when that is done it goes on to redirect to the /posts URL.

In order to redirect we make use of the powerful Router Service that, like any other service, is injected into the component.

Tracked properties

Whenever a tracked property changes, it causes all properties depending on it to recompute.

For example, any time text gets updated upperCaseText will, too.

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

export default class SomeOtherComponent extends Component {

  @tracked text;

  get upperCaseText() {
    return this.text.toUpperCase();
  }

}

We use @tracked to annotate the source property and any derived properties automatically recalculate.

Before @tracked we had a similar mechanism with computed properties. We would “annotate” all derived properties (declaring all dependencies) instead of the source properties:

import Component from '@ember/component';

const { computed } = Ember;

export default Component.extend({

  text: '',

  upperCaseText: computed('text') {
    return this.get('text').toUpperCase();
  }

});

Back to our blog, we need a more readable date.

diagram

We will create a getter for a formatted version of publishedAt. Ember Data attr()s will track automatically, without the need to annotate them with @tracked.

// app/models/post.js

import DS from 'ember-data';
const { Model, attr } = DS;

export default class PostModel extends Model {
  @attr() title;
  @attr() body;
  @attr('date') publishedAt;

  get formattedPublishedAt() {
    return this.publishedAt.toLocaleDateString("en-US");
  }
}

Replacing the property in our component template…

{{! app/templates/components/post-view.hbs }}

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

{{#if @showBody}}
  <span>{{@post.formattedPublishedAt}}</span>
  <pre style="white-space: pre-wrap;">{{@post.body}}</pre>

  <button {{on "click" (action this.removePost @post)}}>
    Remove post
  </button>
{{/if}}

we get the result:

diagram

To finish off, let’s ensure the / URL gets redirected to /posts.

$ ember generate route index
installing route
  create app/routes/index.js
  create app/templates/index.hbs
installing route-test
  create tests/unit/routes/index-test.js

And “replace” the URL in the beforeModel() hook, before model() is ever called, as we are only interested in redirecting away from this route.

// app/routes/index.js

import Route from '@ember/routing/route';

export default class IndexRoute extends Route {
  beforeModel() {
    this.replaceWith('posts');
  }
}

Navigate to http://localhost:4200/ and our app is ready! (For now at least!)


I hope this helped! Code is up on Github at frank06/ember-octane-blog.

Were you able to follow along well? Any questions? Let me know everything in the comments below!

Enjoyed this article? Don't miss my next one!

Join other 3800+ smart developers.

(Time is our most valuable asset. I promise never to waste yours.)