5 Essential Ember Concepts

Last reviewed on June 12, 2019 with Ember Octane

If you just got started with Ember, it can be hard to wrap your head around the framework’s architecture.

Templates, routes, controllers, models, components… Are all of them useful? Which logic belongs where? What’s the relationship between them?

diagram

The Big Picture

The five core concepts are Ember’s fundamental building blocks: routing, models, services, controllers/templates, and components:

diagram

In its simplest form, an Ember app uses the routing layer to resolve, based on the URL, a particular model which is then handed over to a controller/template (which in turn can call components) for display and interaction. A service may be used to retrieve models, for example.

Now that we have a modest idea about how concepts relate to one another, let’s move on with an example:

diagram

What’s going on here?

  1. The router parses the /posts/2 URL and dispatches control to a matching route: the post route, with a parameter post_id=2
  2. The post route invokes its model() hook, which returns a model (Post with id=2) fetched with a service. (The service is called the Ember Data Store. More about this later!)
  3. The route then initializes the post controller/template (that correspond to the post route). It then sets the result of the model() hook on a property of the controller/template. This property is the model property.
  4. The template may optionally invoke a component and feed with some data. Both a template/controller combo or a component are responsible for the user interface: rendering the DOM and handling browser events.

Three models

You might have noticed the word model was mentioned several times above, with slightly different spellings.

  1. model: the concept. It is a representation of a domain entity. More in section Models below.
  2. model(): the hook. Hooks are functions in routes that are automatically called by the framework in a typical request-response cycle. Generally, any time a URL is requested by the browser model() will be called.
  3. model: the property on the controller/template. model() can be asynchronous. Once its value is returned or its promise resolved, the result will be set in the model property of the controller/template.

Great! Now that we understand the flow of a basic application, it’s time to dive deeper.

1. URLs & Routing

The URL is a first-class citizen in Ember. See the diagram above – everything starts at the URL.

It represents the state of the current page’s primary data. For example, a page displaying comments for blog post with ID=5 could naturally have a URL of http://ember.app/posts/5/comments.

Comments are primary data. A tag cloud on that same page loaded by another service would be considered secondary data and shouldn’t belong to the URL.

In order to map URLs to routes we have an object called the Router:

// app/router.js

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

Ember has a mechanism to locate different kind of files (routes, controllers, services, etc). A standard path for PostsPostRoute is app/routes/posts/post.js.

If no route file can be found, Ember will use an auto-generated in-memory default route. This is part of the “convention over configuration” philosophy.

When the URL is requested, the PostsPostRoute will invoke model() from where we return our model:

// app/routes/posts/post.js

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

export default class PostsPostRoute extends Route {
  model({ post_id }) {
    return {
      id: post_id,
      body: "Lorem Ipsum"
    }
  }
});

As an example we’re returning a dummy object. As we’ll see next, there are other more interesting things we can return from model().

Octane news & best practices, straight to your inbox?

Snacks is the best of Ember Octane in a highly digestible monthly newsletter. (No spam. EVER.)

2. Models

A model is a representation of a domain entity. A blog post could be modelled as Post with id, tag, date, text as attributes and comments or author as relationships.

Models can be represented in a JSON format:

{
  id: 2,
  date: '2018-12-12 05:28',
  body: "Lorem ipsum",
  comment_ids: [24, 52, 89],
  author_id: 9
}

One such remote resource can be fetched with the popular axios library:

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

export default class PostsPostRoute extends Route {
  model({ post_id }) {
    return axios.get(`https://app.api/posts/${post_id}`);
  }
});

This is not returning a JSON object though! It is an async call over the network. It returns a Promise.

Promises are objects that represent a future value. They can successfully resolve this value (fulfill) or fail to resolve it (reject).

This is why we were using the word “resolve”: the route will wait until that promise fulfills or rejects before proceeding with the render to template phase.

So the model() hook works with both sync and async!

By far the most common async APIs in this scenario are Ember Data’s findAll and findRecord:

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

The call above will resolve to an instance of Post, which inherits from ember-data/model like all Ember Data models do.

At this point, the post will be set to the post controller’s model property and is ready to be rendered.

3. Services

Did you notice we just called this.store? The way we interact with Ember Data (query, save, etc) is through its store interface.

But where does that store property come from?

That ain’t any Ember Data magic. Under the hood, Ember Data uses an initializer to automatically inject its store service into all routes of the application.

An Ember Service is a long-lived object (singleton) used to provide services to other Ember objects. It directly extends Ember Object but uses its own class name to encourage the services architecture pattern.

We can create our own custom service and have it injected exactly like the Ember Data store – on any Ember object we want!

We might have a tag cloud service that provides us with weighted tags for all the content in our site. For ad-hoc injection we’d use the following syntax:

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

export default class SomeComponent extends Component {
  @service tagCloud;
}

Which will look up a service at app/services/tag-cloud.js and make it available as a tagCloud property on a component, for example.

Services are the preferred way to hold session-wide state in an Ember app.

// app/services/tag-cloud.js

import Service from '@ember/service';

export default class TagCloud extends Service {
  currentTags: [];

  generateCloud() {
    // ...
  };
}

4. Controllers & Templates

So far all we know about controllers is that they are Ember objects initialized by the route, and that resolved models are set on their model property.

Their purpose is to define UI logic around the model, and other behavior such as query parameters.

There is a 1:1 relationship between routes and controllers – even if we haven’t explicitly defined them. (Remember convention over configuration.)

Controllers, like services, are singletons. So the model state will be kept around even when a user is browsing other URLs.

Now back to our previous example, our model (the post) is ready to be rendered. For this, Ember uses Handlebars templates.

Template and controller are essentially the same thing – two sides of the same coin. All the controller properties are the template’s. That’s why the diagram above shows them together inside the same blue box.

// app/controllers/posts/post.js

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

export default class PostsPostController extends Controller {
  @alias('model.body') content;
}
{{!-- app/templates/posts/post.hbs  --}}

<h1>ID is {{ model.id }}</h1>

{{#if content }}
  <p>{{ content }}</p>
{{/if}}

In a very simple Ember app, you might not see a route or a controller. Why? Because they have been auto-generated for you.

When a route loads a model and sets it on its controller, the template has immediate access to it. If there are no computed properties (like content in the example above) or other behavior required, creating a controller file is not necessary.

5. Components

Components take the code/markup combo idea and make it reusable. They consist of a Javascript source file (like a controller) and its corresponding Handlebars template. Again, two sides of the same coin!

Unlike controllers, they are isolated by design. They are not aware of their surrounding context, but they receive data through their attributes, including actions (closure functions).

We can turn the above controller/template combo into a component:

// app/components/show-post.js

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

export default class ShowPost extends Component {
  @alias('model.body') content;
}
{{!-- app/templates/components/show-post.hbs  --}}

<h1>ID is {{ model.id }}</h1>

{{#if content }}
  <p>{{ content }}</p>
{{/if}}

This component can be invoked from any template using the <ShowPost> syntax:

{{#each posts as |post|}}
  <ShowPost @model=post />
  <EditAuthor @model=post.author @onAfterSave=(action 'sendAnalytics') class="green-box" />
{{/each}}

The EditAuthor component in this example receives data attributes, an action, and a plain HTML attribute.

Data owner

In a nutshell, we say that a model has an owner. Certainly a contentious definition, but generally speaking the controller or component that lists the model’s attributes for mutation is considered the owner of that model.

The model owner is the object used to persist the model to the backend (via Ember Data Store or other mechanism).

In our case, one could argue that the controller owns the blog post model and, in our last example, the EditAuthor component owns the post’s author model.

Here’s a table comparing controllers and components:

ControllerComponent
Javascript side of a templateYesYes
Can be called from any templateNoYes
State spanSingleton (held during session)Ephemeral (lost on rerender)
Query paramsCan hold QP stateNo
Useful hooksNo, has to use observersYes, didInsertElement, didUpdateAttrs among others
You might have heard that controllers were being removed? Nope, controllers are still a part of Ember.

Octane news & best practices, straight to your inbox?

Snacks is the best of Ember Octane in a highly digestible monthly newsletter. (No spam. EVER.)

Data Down, Actions Up

Let’s expand on this idea of passing actions as attributes.

We’re going to modify our previous blog post example:

// app/controllers/posts/post.js

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

export default class PostsPostController extends Controller {
  @alias('model.body') content;

  @action
  async removePost(model) {
    await model.destroyRecord();
    this.transitionToRoute('/');
  }
}
{{!-- app/templates/posts/post.hbs  --}}

<h1>ID is {{ model.id }}</h1>

{{#if content }}
  <p>{{ content }}</p>
{{/if}}

<button {{action this.removePost model}}>
  Remove post
</button>

Updating our diagram to reflect the removePost action:

diagram

Do you notice that data flows down and actions up? This is an architectural pattern we call Data Down, Actions Up (“DDAU”).

Understanding these 5 concepts is a fantastic start! Good Ember apps are architected this way.

Getting Started with Ember Octane is a great way to put in practice what we just discussed.

What Ember concept are you struggling with? Let me know in the comments below!

Enjoyed this article? Join Snacks!

Snacks is the best of Ember Octane in a highly digestible monthly newsletter. (No spam. EVER.)