Last reviewed in February 2020 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

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

5 Essential Ember Concepts You Must Understand. This tutorial will cover essential Ember concepts. If you want to understand them in detail, I recommend reading

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!

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:

dependency management via npm

file generators from blueprints

static server with backend proxying and live browser reloading

testing tools

a vibrant ecosystem of 5000+ add-ons

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.16.0 node: 13.6.0 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

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!

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.

Octane news & best practices, straight to your inbox? Success! You're subscribed! An error has occurred. Please e-mail me@frank06.net with your request. Snacks is the best of Ember Octane in a highly digestible monthly newsletter. (No spam. EVER.)

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-cli-mirage installing ember-cli-mirage create /mirage/config.js create /mirage/scenarios/default.js create /mirage/serializers/application.js Installed addon package.

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 Model , { attr } from '@ember-data/model' ; export default class PostModel extends Model { @ attr title ; @ attr body ; @ attr ( 'date' ) publishedAt ; }

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

{{! app/templates/posts.hbs }} < ul > {{#each @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).

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

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 = {{fn this . addPost } } / > < ul > {{#each @model as |post|}} < li > {{post.title}} < / li > {{/each}} < / ul > {{outlet}}

Okay, what's going on?

We are using the built-in Ember Input component.

this.newTitle : the property on the controller to bind to the newly input title

: 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!

Octane news & best practices, straight to your inbox? Success! You're subscribed! An error has occurred. Please e-mail me@frank06.net with your request. Snacks is the best of Ember Octane in a highly digestible monthly newsletter. (No spam. EVER.)

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 neatly display the title, date and body.

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 = {{@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 @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)

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 )

would refer to a property on the component class (for example ) {{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:

Clicking on the first item should load /posts/1 …

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 @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. 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 . 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 .

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 " ( fn this . removePost @ post ) } } > Remove post < / button > {{/if}}

( on and fn are modifiers in action.)

Naturally, we have to implement this.removePost :

(You might need to create this component JS file.)

// 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.

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 Model , { attr } from '@ember-data/model' ; 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:

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!