Getting Started with Ember


Goal: Build an app that will record a list of todos.
Time: 2 hours (let me know if it's very different for you)
Last Update: 7th September 2015.

Introduction

I'm writing the tutorial that I wish I had 24 months ago when I was starting out with Ember. The tutorial that doesn't assume that I've been using jQuery for 5 years, know what state is, eat 'promises' for breakfast or even understand the term 'hook'. It's a tutorial for beginners, beginners to coding, beginners to JavaScript or beginners to front end frameworks. If you are not a beginner, this tutorial might be a bit slow for you, but I invite you to read it and help me improve it. Whatever your level please let me know where you get stuck, what things are not clearly explained or lack sufficient detail. Most easily reached by tweet or leave a comment at the bottom.

Ember is a front end framework for creating ambitious apps. It provides a lot of functionality that is common for web applications, so you can concentrate on writing your app and not solving problems that have already been solved.

One final warning, I'm not as funny as I think I am.

Getting Started

First we need to install node.js. Node.js is javascript used for writing applications on a server. We won't be doing anything directly with node, but will use node package manager (npm) to install ember CLI (command line interface). If the last two sentences went completely over your head and you're like, 'WTF? I thought he said this was a beginners tutorial on ember, why's this jackass chatting about node and package managers, what's a package manager? How is he predicting my thoughts so accurately...' don't worry, it's not important for the tutorial, just get node installed from node.js. Once installed, if you open a terminal and type.

node -v  

You should see the version (-v) of node.js e.g. v6.x.x.

Now lets use npm to install ember CLI which will give us all of the ember goodness.

npm install ember-cli -g  
ember -v  

The -g on npm install, installs the package globally, compared to in the folder we are currently in. This means we can use the ember-cli from anywhere on the computer. In this tutorial I've used version: 2.10.x of ember-cli, and also for ember & ember-data. Use this version or higher for this tutorial.

Navigate to a folder in the terminal where you want to keep the app. Using ember CLI to make a new app is as easy as.

ember new todo-mvc  

If you've used rails before you'll be familiar with this type of generator. We've basically told ember-cli to set up a load of files and install the required other packages of code to make an ember app. There will be a lot of output from your terminal, the upshot of it being your new app is created in a todo-mvc folder so navigate to it.

cd todo-mvc  

Take some time to look at the structure of your app. Most of the files we'll be using or creating will be under the app folder. At this point I would suggest opening the contents of that folder in a text editor, sublime text or atom (Mac only) are good options.

Lets start the app.

ember serve  

The output from your terminal will be this.

Livereload server on http://localhost:49152
Serving on http://localhost:4200/


// Some build information about the time it took to build //

The important line is Serving on http://localhost:4200 this means that if you navigate to localhost:4200 in your browser you should see your ember app. You should see this page:

Great! A friendly message from the ember team. You could checkout the Quick Start, but why would you want to do that? You've got the Painfully Long & Indepth Start right here. The Ember Guides are a great resource to either get to grips with Ember or to further your understanding, although when I first started with Ember I found them a little daunting and sometimes incomprehensible. That was back in early 2015 and since then the guides have improved and so have I. I now find the guides & API documentation the first place I turn to if I'm stuck with anything in Ember.

Let's go ahead though and remove this welcome page, and in the process start to use ember to build our web app. To remove the welcome page, the instructions are there on the bottom of the page.

To remove this welcome message, remove the ember-welcome-page add-on from your package.json file

Open up package.json in your text editor and remove this line:

"ember-welcome-page": "^1.0.1",

Once we've removed this line, stop the server with Ctrl + C in the terminal where it's running. Now we tell our package manger to remove the packages we've removed.

npm prune  

And restart the server with $ ember serve again.

You should now see a blank page. Congratulations!!

Seriously let's do something now. At the base of all Ember apps is the application route. In the next section I'll explain routes in more detail, but for now if we add an application template, it will get loaded when the application starts. Now we can add a template file by simply creating a new file in the right place, but ember provides something called a generator to automatically create files for us. When creating one file a generator isn't hugely necessary, but when you want to create multiple files in different locations, with some starter code in them, these generators are a life saver, especially when starting Ember and not entirely confident where new files should go. In the terminal type:

ember generate template application  

Which will have the following output:

installing template
  create app/templates/application.hbs

The generator has simply created one file for us in the app/templates folder. The hbs extension is a handlebars, which is a templating language. Ember further extends handlebars with something called HTMLbars which adds ember specific functionality. Templating languages allow you to perform some basic code in your html. For example output a variable, loop over an array or check if some variable is true or false. We will do all these things in this tutorial so don't worry if that doesn't make sense right now.

<h2>Welcome to the Jungle</h2>  

Check the browser and your Ember app should have automatically reloaded with your new title.

The Router && Routes

Our first bit of proper Ember. I'm excited.

Together the router and routes determine the structure of our app and how it responds to users. When a user visits www.myapp.com/about, what do we want it to do? If they visit www.myapp.com/todos/1 what do we want to show at this URL? In these examples we'd want to load content about the app for the first url and fetch the todo of an ID of 1 from our database for the second url.

There are two concepts we need to get our head around in order to understand routing in Ember and we really should take the effort to understand this because it is, in my opinion, what sets apart ember from other frameworks, it's what makes it a framework for building ambitious web apps.

  1. Route: A route will load the template and data required.
  2. Router: The router determines which Route (or set of Routes) to load based on a given URL.

Let's look at this process with our todos route. First, download the Ember Inspector which we will need throughout the tutorial. It is a chrome or firefox addon. It will appear in developer tools as a tab (open with cmd+alt+i chrome mac). We'll be using it in a bit.

Open up our router, app/router.js. That's right, the router is so important its not even hidden in a folder. Lets add our 'todos' route inside the Router.map.

import Ember from 'ember';  
import config from './config/environment';

var Router = Ember.Router.extend({  
  location: config.locationType
});

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

export default Router;  

We now have a todos route the '/' URL (e.g. www.localhost:4200/). Why '/'? This is the path we specify in the route definition. We could put it at '/shitToDo' path or we could leave it blank and then it would be at the '/todos' path by default. We are telling our application, if someone visits this URL, load the todos route. We will create that route object shortly, which will tell the app what template and data to load. Finally the last line export default Router; is part of the latest release of Javascript that means you don't get global variables. Global variables are bad, if you don't know why, you can google it or just trust me when I say...

Open up developer tools on our application page in the browser and hit the Ember tab. Lets look at our routes.

s

I've checked 'Current Route only', this is to reduce the noise. We have an application route and underneath this is our todos route. Application route you get for free, it's at the top of all route trees in Ember. The application route its a good place to put anything you want to be on every route in your app, for example a header and footer. We've already seen the application template, app/templates/application.hbs. Lets edit it now.

<section id="todoapp">  
    <header id="header">
        <h1>todos</h1>
    </header>

    {{outlet}}
</section>

<footer id="info">  
    <p>Double-click to edit a todo</p>
</footer>  

We have some html with one variable, {{outlet}}. I will explain what this variable is in the next section of the tutorial, #keepReading.

Where is the application route? It's not in the routes folder, and this is because it's auto generated by Ember. We could create it if we wanted non-default behaviour. The default behaviour of a route is to render the template with the same name.

Application Route -> Renders Application Template
app/routes/application.js Route -> Renders app/templates/application.hbs Template

The other function of the route is to load the data, we don't require any data to be loaded at the application route level, so we can move on without creating an application route.

Todos Route

Let's mimic the application route for our todos route. Create a todos template in the templates folder.

touch app/templates/todos.hbs  

Inside this file add some html, <h2>Todos Template</h2>. Now when we look at our app, we see that content. Templates are nested within one another. We have a tree structure.

Application -> Todos

Routes are loaded in order, templates are nested using the {{outlet}} in the parent template. You can see a visual representation of this in the Ember Inspector tab under View Tree.

Lets look at the data part of a route. We should create a todos route.

touch app/routes/todos.js  
import Ember from 'ember';

export default Ember.Route.extend({  
    model() {
        let todos = [
            {
                title: "Learn Ember",
                complete: false,
            },
            {
                title: "Solve World Hunger",
                complete: false,
            }
        ];
        return todos;
    }
});

Our todos route has a model hook. The term hook is synonymous for function. This model function will be run when the route is loaded. Inside our model function we create an array [] of javascript objects {} that represent a todo. The array is called todos and this is returned by the function. This means that our todos route has now rendered the todos.hbs template (by default) and it's also loaded our todos data. The last part is to show that the template has access to the model data. Open up app/templates/todos.hbs.

<h2>Todos Template</h2>

<ul id="todo-list">  
    {{#each model as |todo|}}
        <li>
            {{todo.title}}
        </li>
    {{/each}}
</ul>  

You should see something like this.

The model array is being looped over with the handlebars {{#each}} helper. Each instance we are printing out the {{todo.title}}. The model hook from the route sets the variable to be called model by default for the template, again we could change it to be more explicit, like 'todos', but we won't right now.

Those last two sections were a lot of information. I would recommend re-reading them to cement your understanding of how the router and routes work in Ember.

Then a bonus bit of information. We created some files manually here, we added some boiler plate code like import Ember from 'ember'. We could have used a generator to do this instead.

ember generate route todos  

Which would have output:

installing route
  create app/routes/todos.js
  create app/templates/todos.hbs
updating router
  add route todos
installing route-test
  create tests/unit/routes/todos-test.js

Creating the route file, the template file and updating the router for us. It also would have created a unit test for our route. We're not touching tests in this tutorial, I explain why later.

A style break

We've both been thinking it, but neither one of us has had the balls to say it yet. It's unclear if it's your fault, or if it's my fault, but I've got to say it now... The app looks shit.

As with so many issues, once it's out on the table and we've talked about about it, it turns out there is a simple solution, so well done us for bringing it up.

Find the CSS here, copy & paste it into app/styles/app.css.

Let's put in the html content to match the style sheet for todos template, app/templates/todos.hbs.

<input type="text" id="new-todo" placeholder="What needs to be done?" />

<section id="main">  
    <ul id="todo-list">
        {{#each model as |todo|}}
            <li class="completed">
                <input type="checkbox" class="toggle">
                <label>{{todo.title}}</label><button class="destroy"></button>
            </li>
        {{/each}}
    </ul>

    <input type="checkbox" id="toggle-all">
</section>

<footer id="footer">  
    <span id="todo-count">
        <strong>2</strong> todos left
    </span>
    <ul id="filters">
        <li>
            <a href="all" class="selected">All</a>
        </li>
        <li>
            <a href="active">Active</a>
        </li>
        <li>
            <a href="completed">Completed</a>
        </li>
    </ul>

    <button id="clear-completed">
        Clear completed (1)
    </button>
</footer>  

Now your app looks so damn good.

Planning our Routes

The more I've done Ember, the more I've realised how important it is to plan your applications routes at the beginning. I don't know if it's realistic to do this when starting a new project, but in this case I know the exact end result so I'm going to do it.

We need three routes nested under our todos. index, incomplete and complete. The first route will list all the todos. The other two routes will filter the todos accordingly. They will have the following URLs.

www.localhost:4200/ -> Index Route
www.localhost:4200/incomplete -> Incomplete route
www.localhost:4200/complete -> Complete route

We can use the ember-cli generator tool to make a route file, template and make the appropriate entry in the router.js file. In the terminal.

ember generate route todos/index  
ember generate route todos/complete  
ember generate route todos/incomplete  

Which will output.

installing route
  create app/routes/todos/complete.js
  create app/templates/todos/complete.hbs
updating router
  add route todos/complete
installing route-test
  create tests/unit/routes/todos/complete-test.js

For each route. The generator will generate all the files we need and a change our router.

Our final app/router.js should look like this.

import Ember from 'ember';  
import config from './config/environment';

var Router = Ember.Router.extend({  
    location: config.locationType
});

Router.map(function() {  
    this.route('todos', { path: '/' }, function() {
        this.route('complete');
        this.route('incomplete');
    });
});

export default Router;  

We have nested two routes under our todos route. The index route is not listed, but in Ember a nested set of routes will automatically have an index route. Unless a different path argument is provided the URL will be the same as the name of the route, e.g. '/complete'.

The last thing we need to do is to move the list of todos from the todos template, app/templates/todos.hbs, down to the index template, app/templates/todos/index.hbs. Remember we use an {{outlet}} in the parent template to indicate where the child template should be loaded, and the todos route is the parent of index, complete and uncomplete routes, which means any content in there will be loaded on all three of these routes, so lets edit app/templates/todos.hbs.

<input type="text" id="new-todo" placeholder="What needs to be done?" />

<section id="main">  
    {{!-- This is where our todos will go --}}
    {{outlet}}

    <input type="checkbox" id="toggle-all">
</section>

{!-- The rest of the todos mark up truncated for brevity --}

Now create the index template and add the todos list on it, app/templates/todos/index.hbs.

<ul id="todo-list">  
    {{#each model as |todo|}}
        <li class="completed">
            <input type="checkbox" class="toggle">
            <label>{{todo.title}}</label><button class="destroy"></button>
        </li>
    {{/each}}
</ul>  

Our index route will use the model for the todos route. Open app/routes/todos/index.js.

import Ember from 'ember';

export default Ember.Route.extend({  
    model() {
        return this.modelFor('todos');
    }
});

The 'modelFor' function finds the model for another Route, in this case the for the parent 'todos' route.

Now go look at www.localhost:4200/ and open Ember Inspector and look at the view tree.

You will see that the index template is being rendered under the todos route.

We now have a solid foundation for our app.

Components

Components are discrete reusable blocks of code that produce things (components) that users can interact with. For example an input box is a component which you see in use all over the internet. It has standard behaviours, i.e. allowing the user to write in it. In any html we can generate this input box by using the <input> tag. the development community have come to realise that these common components are really useful and if we can allow any developer to write new ones, that could also be useful, for example a standard <calendar> tag would be useful on many websites.

A (standard for components)[http://w3c.github.io/webcomponents/spec/custom/] is currently being developed by the powers that be. Ember implements components and tries to stay close to the predicted standards so in the future you can easily switch in the standard components.

Let's think about the components in our todos application. It seems to me that we have two major components in our application. A todo-list and a todo-item.

The todo-list should perform the following functions.

  • display how many todos are left
  • toggle between complete and incomplete todos
  • clear all complete todos

The todo-item should perform the following functions.

  • display the todo item title and indicate its state
  • mark the todo as complete or incomplete
  • edit the todo title
  • delete the todo

Lets generate our todo-item component first.

ember generate component todo-item  
installing component
  create app/components/todo-item.js
  create app/templates/components/todo-item.hbs
installing component-test
  create tests/integration/components/todo-item-test.js

Our component consists of two parts, a template, app/templates/components/todo-item.hbs, for displaying it and a javascript file, app/components/todo-item.js, for calculating any variables and handling user events.

A todo-item template contains the html mark up. Open up app/templates/components/todo-item.hbs.

<input type="checkbox" class="toggle" checked="{{if todo.complete 'checked'}}">  
<label class="{{if todo.complete 'completed'}}">{{todo.title}}</label><button class="destroy"></button>  

Now in our app/templates/todos/index.hbs.

<ul id="todo-list">  
    {{#each model as |todo|}}
        {{todo-item todo=todo}}
    {{/each}}
</ul>  

In our todo-item we are outputting the {{todo.title}} variable so we need to make sure we pass in a todo when we use the component. On the todos.index template you can see we are doing this inside the {{#each}} block. We are also checking the status of todo.complete and if it is, checking the box and applying a class of completed with checked="{{if todo.complete 'checked'}}" and {{if todo.complete 'completed'}}.

There is one final thing we need to do in the javascript for our component, app/components/todo-item.js.

import Ember from 'ember';

export default Ember.Component.extend({  
    tagName: 'li'
});

Every component will be inserted in a html tag by ember, by default this would be <div>, but in this case we wanted a <li> to surround our component so we specified that here with tagName property. We could have put the <li> in the template, which might have been clearer but leads to spurious extra divs (mostly harmless) in the code.

Now lets make the todo-list.

$ ember g component todo-list

*g is a shortcut for generate

Our todo-list template will contain most of the mark up from our todos template, lets edit app/templates/components/todo-list.hbs.

<section id="main">  
    {{yield}}

    <input type="checkbox" id="toggle-all">
</section>

<footer id="footer">  
    <span id="todo-count">
        <strong>2</strong> todos left
    </span>
    <ul id="filters">
        <li>
            <a href="all" class="selected">All</a>
        </li>
        <li>
            <a href="active">Active</a>
        </li>
        <li>
            <a href="completed">Completed</a>
        </li>
    </ul>

    <button id="clear-completed">
        Clear completed (1)
    </button>
</footer>  

The {{yield}} helper is similar to {{outlet}}. It will allow us to inject other components and markup into this one. See this in practice by opening app/templates/todos.hbs.

<input type="text" id="new-todo" placeholder="What needs to be done?" />  
{{#todo-list todos=model}}
    {{outlet}}
{{/todo-list}}

The {{outlet}}will be rendered in the {{yield}} location of our todo-list component. Remember the {{outlet}} is our nested route, todos.index. We are also passing in the todos model to our todo-list now, we will write code that uses the model later in this tutorial.

Modelling a Todo

A model is like a blueprint for your data. Each todo item in our list will be an instance of our Todo model. We can use our ember generator to generate a model for us.

$ ember generate model todo title:string complete:boolean

Open app/models/todos.js to view our generated model blueprint.

import DS from 'ember-data';

export default DS.Model.extend({  
    title: DS.attr('string'),
    isCompleted: DS.attr('boolean')
});

Each instance of our Todo model will have a title and an isCompleted field. The title field will be a string and the isComplete field will be a boolean (true or false). We are using ember data, which is the library used to manage data for ember apps. It's maintained by the ember team and should work out of the box.

Data Mirage

Previously our model data was an array of javascript object. This was a quick and dirty fix for an example but when building an app, we will usually request data from a server. We will not build a server in this tutorial, what we will do is use a package called ember-cli-mirage that will mock a server for us. We can install it easily with ember-cli.

$ ember install ember-cli-mirage

Now in app/routes/todos.js we will use ember data to request data instead of using the array variable.

import Ember from 'ember';

export default Ember.Route.extend({  
    model() {
        return this.store.findAll('todo');
    }
});

Now ember data will send a REST GET request to /todos. I won't explain REST and GET requests in this tutorial if you don't understand them, don't worry you don't need to understand it for now.

We will set up Mirage to deal with this request to todos and in fact all the requests our dummy server will need to deal with for this tutorial.

Edit mirage/config.js.

export default function() {  
    this.get('/todos');
    this.post('/todos');
    this.patch('/todos/:id');
    this.del('/todos/:id');
}

This config sets up a server that will respond to calls from Ember Data to get all todos, create a todo, edit a todo and delete a todo.

We need a mirage model, which is a mirror of our ember model, easily created with a generator.

$ ember g mirage-model todo

We create a factory which will generate the data.

$ ember g mirage-factory todo
import Mirage, {faker} from 'ember-cli-mirage';

export default Mirage.Factory.extend({  
    title(i) { return `Todo title ${i + 1}`; },
    complete: faker.list.random(true, false)
});

This code will create a todo with title "Todo title 1" and randomly selected complete state of true or false.

We call the factory in app/mirage/scenarios/default.js.

export default function(server) {  
    server.createList('todo', 3);
}

The scenario instructs our dummy server to create 3 todos.

Finally restart your server in the terminal.

And that's it. We are now mocking our data. If you didn't follow that, it's probably because I didn't go through it in great detail. Don't worry about it, learning Mirage not the main goal of this tutorial.

Create a New Todo

Lets add another todo to our list. In order we need to do this we need to create a new component, todo-input.

$ ember generate component todo-input

This component's template app/templates/components/todo-input.hbs contains an handlebars input helper.

{{input type="text" id="new-todo" placeholder="What needs to be done?" 
    value=title enter="submitTodo"}}

Here we issue an action called "submitTodo" which will be triggered when the user hits enter on the input. We need to handle that action on the components javascript app/components/todo-input.js.

import Ember from 'ember';

export default Ember.Component.extend({  
    store: Ember.inject.service(),
    actions: {
        submitTodo() {
            if (this.get('title')) {
                this.get('store').createRecord("todo", {
                    title: this.get('title'),
                    complete: false
                });
            }
            this.set('title', "");
        }
    }
});

Firstly we inject the store into the component. The store is part of ember-data, it contains data and allows us to query for more data. In the submitTodo action we are checking that there is a title and if there is we tell the store to create a new record for us with the value from the input field. Then we clear the input with this.set('title', '');.

Complete a Todo

We want to be able to toggle the value of complete from true to false and back again through our app for a todo. This requires change to our checkbox input, we will use another handlebars {{input}} helper. Our component app/templates/components/todo-item.hbs will change to.

{{input type="checkbox" checked=todo.complete class="toggle"}}
<label class="{{if todo.complete 'completed'}}" {{action "editTodo" on="doubleClick"}}>{{todo.title}}</label>  
<button class="destroy"></button>  

When the input tag is rendered it uses the current value of the todos complete property to determine whether the input is checked or not, checked=todo.complete. When the check value is changed, Ember will update the model value for us without having to write any code. Yep, the toggle action is automatically handled for us, how cool is that?!? (Very)

As a quick sanity check, we should now be able to do 2 things from our initial list of requirements for the todo-item component:

  • it should display the todo item
  • it should have a way to mark the todo as complete or incomplete
  • it should be editable (TODO)
  • it should allow us to delete the todo (TODO)

If only I had some kind of todo list to keep track of these requirement!

This behaviour is shown in the app below (from the ember guides).

Ember.js • TodoMVC

Edit and Delete Todos

We need to tell our app when we are editing a todo. For this we need a property, isEditing, on our component app/templates/components/todo-item.hbs.

{{#if editing}}
    <input class="edit">
{{else}}
    {{input type="checkbox" checked=todo.complete class="toggle"}}
    <label class="{{if todo.complete 'completed'}}" {{action "editTodo" on="doubleClick"}}>{{todo.title}}</label>
    <button class="destroy"></button>
{{/if}}

We toggle the editing class on the isEditing property. We've used a block helper, {{#if}}. This gives us a simple if else block. If isEditing is true then render <input class='edit'>, if isEditing is false then render the other part after the {{else}}. It's important to note that in handlebars you can only check whether a property is true or false* and there is no else if option. Inside the else block we have another {{action}} helper. The action is called editTodo and is triggered by a doubleClick on the element.

* To extend the if block check out the truth helpers addon.

If you double click the todo, you'll see a failed to handle action error in the console, just as we did with the createTodo action.

Uncaught Error: <[email protected]:todo-item::ember543> had no action handler for: editTodo

Let's handle the action in our components javascript, app/components/todo-item.js.

import Ember from 'ember';

export default Ember.Component.extend({  
    tagName: 'li',
    classNameBindings: ['editing'],
    editing: false,
    actions: {
        editTodo() {
            this.toggleProperty('editing');
        }
    }
});

First notice that we create a property on our component called editing. This is a display state of the todo-item component, which means it's not important to the rest of the wider application, so we manage it on the component. Components should be responsible for managing their own state. The action, editTodo, toggles the editing property. Finally classNameBindings will apply a class 'editing' to the <li> enclosing tag of our component. This is a good opportunity to checkout the API docs for Ember.Component which will explain how classNameBindings property works.

Now double click now renders an input box.

The next step is to change in <input> tag into a handlebars helper that will change the title value of the model. Our component will now look like this in app/templates/components/todo-item.hbs.

{{#if editing}}
    {{input class="edit" value=todo.title action="submitTodo"}}
{{else}}
    {{input type="checkbox" checked=todo.complete class="toggle"}}
    <label class="{{if todo.complete 'completed'}}" {{action "editTodo" on="doubleClick"}}>{{todo.title}}</label><button class="destroy"></button>
{{/if}}

Our submitTodo action on the app/components/todo-item.js.

actions: {  
    editTodo() {
        this.toggleProperty("editing");
    },
    submitTodo() {
        const todo = this.get("todo");
        if (todo.get("title") == "") {
            todo.destroyRecord().then(() => {
                this.toggleProperty("editing");
            });
        } else {
            this.toggleProperty("editing");
        }
    }
}

In this action we get the todo item, we then check whether the title is empty or not. If the title is empty we delete the todo, and once the server has responded, indicated by then, we toggle the editing property, if the title isn't empty we toggle the editing property.

Lets just hook it up to the 'x' button on the right of the todo. We need add another {{action}} to the handlebar helper for our destroy button in app/templates/components/todo-item.hbs.

<button class="destroy" {{action "deleteTodo"}}></button>  

We've added a deleteTodo action for a button. We need to make our javascript send this action to the todos route from our components actions, open app/components/todo-item.js.

actions: {  
    editTodo() { /* truncated */ },
    submitTodo() { /* truncated */ },
    deleteTodo() {
        this.get("todo").destroyRecord();
    }
}

Nested Routes

At the beginning of the tutorial we created nested routes in our router. Lets use them to filter out the complete and incomplete todos. Let's first do this in our complete route, app/routes/complete.js.

import Ember from 'ember';

export default Ember.Route.extend({  
    model() {
        let todos = this.modelFor('todos');
        return todos.filter((todo) => {
            return todo.get('complete')
        })
    },
    renderTemplate(controller, model) {
        this.render('todos.index', {
            model: model
        });
    }
});

Now if you visit localhost:4200/complete you will see only the complete todos on our list. The model() hook first returns the todos model with todos.filter('todo', then it goes through each todo and returns the ones where todo.get('complete') === true. The renderTemplate() hook tells ember to render the template for todos.index (app/templates/todos/index.hbs) and apply the model for this route as the model.

Our incomplete route will be almost identical, but with return !todo.get('complete'); in the filter function, our app/routes/incomplete.js file looks like this.

import Ember from 'ember';

export default Ember.Route.extend({  
    model() {
        return this.store.filter('todo', function(todo) {
            return !todo.get('complete');
        });
    },
    renderTemplate(controller, model) {
        this.render('todos.index', {
            model: model
        });
    }
});

Now we need a way for the user to reach these routes. On our todos-list we need to make some links to our routes, app/templates/components/todo-list.hbs.

<ul id="filters">  
    <li>
        {{#link-to "todos.index" activeClass="selected"}}All{{/link-to}}
    </li>
    <li>
        {{#link-to "todos.incomplete" activeClass="selected"}}Active{{/link-to}}
    </li>
    <li>
        {{#link-to "todos.complete" activeClass="selected"}}Complete{{/link-to}}
    </li>
</ul>  

The {{#link-to}} helper allows us to create links to our applications routes, as defined by the router, and we can apply a custom class when the route is active, i.e. when we are at localhost:4200/incomplete that link will have a class of selected applied to it.

Here is an example of the functionality of the app so far.

Ember.js • TodoMVC

Finishing touches for Todos

In this final section we will do three things.

  1. Track the number of incomplete todos.
  2. Add a button to clear the complete todos.
  3. Toggle all todos between complete and incomplete.

Now we can deal with the 3 finishing touches which will all be handled by our todo-list component.

First lets change the hardcoded value of '1' to a property of the todo-list component called {{remaining}} and add a property {{inflection}} in our app/templates/components/todo-list.hbs.

<span id="todo-count">  
    <strong>{{remaining}}</strong> {{inflection}} left
</span>  

Let's add the properties to app/components/todo-list.js.

import Ember from 'ember';

export default Ember.Component.extend({  
    remaining: Ember.computed('todos.@each.complete', function() {
        let todos = this.get('todos');
        return todos.filterBy('complete', false).get('length');
    }),
    inflection: Ember.computed('remaining', function() {
        var remaining = this.get('remaining');
        return (remaining === 1) ? 'item' : 'items';
    })
});

The remaining property contains an Ember computed property. A computed property is a property calculated using another property or a number of properties. Ember computed properties will observe properties and if any of them change, it will update the computed property.

In our remaining property, we are observing todos.@each.complete. This means that we are looking at every instance (@each) of the todos and the complete property of each instance. This means that if the complete property changes on any of the todos, the computed property will recalculate. If you changed the title property of a todo, this function would not recalculate. The actual computed property is the number (.get('length')) of models where complete is false (model.filterBy('complete', false).

In our inflection property we our observing our other computed property, remaining. The computed property will be 'item' if there is only one todo remaining and 'items' in all other cases.

In order to add a button to clear the the complete todos for part two we need to figure out if we have completed any todos, how many we've completed and write an action to delete the completed ones. First the changes in our app/components/todo-list.js.

import Ember from 'ember';

export default Ember.Component.extend({  
    remaining: Ember.computed('todos.@each.complete', function() { /* truncated */}),
    inflection: Ember.computed('remaining', function() { /* truncated */ }),
    completed: Ember.computed('todos.@each.complete', function() {
        var todos = this.get('todos');
        return todos.filterBy('complete', true).get('length');
    }),
    hasCompleted: Ember.computed('completed', function() {
        return this.get('completed') > 0;
    }),
    actions: {
        clearCompleted() {
            let completed = this.get('todos').filterBy('complete', true);
            completed.forEach((todo) => {
                this.sendAction('deleteTodo', todo);
            });
        }
    }
});

First we have an action, clearCompleted, which gets all the completed todos using filterBy method, which we've seen previously. We then delete each todo.

We've added two computed properties, hasCompleted, which finds out if we have this.get('completed') > 0 i.e. at least 1 completed todo. This property will be used in our template to allow us to decide whether or not to show the delete completed todos button using a handlebars {{#if}} block. The second computed property, completed, counts the number of completed todos. We also need to add the action to the button.

Lets add the necessary components to our app/templates/components/todo-list.hbs.

{{#if hasCompleted}}
    <button id="clear-completed" {{action "clearCompleted"}}>
        Clear completed ({{completed}})
    </button>
{{/if}}

Final interaction, home straight, if you've made it here, don't give up now, if you've not made it here...

For this final functionality of toggling todos between complete and incomplete lets start with the components HTML and then work out what our javascript needs to do. Open up app/templates/components/todo-list.hbs.

<section id="main">  
    {{yield}}

    <input type="checkbox" id="toggle-all" checked={{allAreDone}} onclick={{action "toggleAll" value="target.checked"}} />
</section>  

We've added a checkbox {{input}} in handlebars. This will toggle the allAreDone property.

Lets use this property on our javascript to set all the todo to complete or not complete, app/components/todo-list.js.

import Ember from 'ember';

export default Ember.Component.extend({  
    /* truncated for brevity */
    didInsertElement() {
        let todos = this.get('todos');
        if (todos.get('length') > 0 && todos.isEvery('complete', true)) {
            this.set('allAreDone', true);
        } else {
            this.set('allAreDone', false);
        }
    },
    actions: {
        toggleAll(completeValue) {
            this.set("allAreDone", completeValue);
            let todos = this.get('todos');
            todos.forEach((todo) => {
                todo.set('complete', completeValue);
            });
        }
    }
});

Here we use the didInsertElement() hook, which is called the first time a component is inserted. In this function we calculate if there are any todos, todos.get('length') > 0, and,&&, if they are all complete,todos.isEvery('complete', true)then we setallAreDoneto true. In all other casesallAreDone` will be false.

When the checkbox is clicked, the triggered action sets the allAreDone property and sets all the todos to that status (true or false).

WOAH that's it, we're done. HIGH 5.

Conclusion

All good things must come to an end, but you are no where near the end, you're at the beginning of building ambitious web apps with ember.js. I've covered some of the basics of ember.js, but there is much more ember goodness out there to be consumed.

I've put the code on github for you to download.

Thanks for taking the time to read and complete this tutorial. If you've enjoyed it, I'd really appreciate a cheeky tweet.