Getting Started with Ember.js using Ember CLI


Note: This is by far my most popular post, but as with a lot of ember related material it's really fallen behind. I plan to update it soon, it's now 20th Jan 2016, but at the moment, because of version changes etc. the tutorial is not as straight forward as I would like for people. If you are experienced/persistent/gifted/a pixie you can usually work around errors, but if you can't, please accept my apologies. I will update this!!!

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 12 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. v0.12.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: 1.13.x of ember-cli. 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

First if you are using ember-cli version before 2.0.0 you will need to change the version of ember and ember-data to 2.0.0 in the bower.json file. At the time of writing ember-cli was still on version 1.13.8.

"ember": "2.0",
"ember-data": "2.0",

You will need to re-install the ember packages. You can do this by typing the following in the terminal.

$ bower install

Bower may ask you which version of ember to install with a couple of options, read the options and select the one that includes ember 2.0 or higher.

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.

version: 1.13.8
Livereload server on port 35729
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 a page with:

Welcome to Ember.js

Let's change that message, open up the file app/templates/application.hbs. 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 id='title'>Welcome to Ember-CLI tutorial :)</h2>

{{outlet}}

Change the text within the h2 tag to be whatever you want. It will automagically change in the browser. Now we're ready to get going with Ember. Woop Woop.

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 and fetch the todo of an ID of 1 from our database respectively.

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 is 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 ember CLI magic 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.

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
Route app/routes/application.js -> 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 means a function. This model hook 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>  
    {{#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.

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

version: 1.13.8
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'.

You will also have noticed that we generated test files for each route...

Tests are out of scope for this tutorial, which is a euphemism for:

I've not learnt how to write tests so I can't include them in this tutorial.

If you don't know what tests are in a programming sense they are basically code that will test your expectations for an app. For example we expect to be able to add a new todo to our list, if we were well rounded and experienced developers we would write a piece of code that made a computer fill out the input box and hit enter, the code would then check that this added the todo to our list. This test would fail as we've not written any code to do that. We would then write the code that made this test pass, the really useful part is that should we change things in the future that break this functionality, our tests would tell us automatically without us having to 'manually' test adding a new todo every time we changed the code. So hopefully you have a better understanding of what testing is and why it's useful, even if you are no closer to knowing how to write the damn things.

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. Rembmer we use an {{outlet}} in the parent template to indicate where the child template should be loaded.

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');
    }
});

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 could 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

version: 1.13.8
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 app/mirage/config.js.

export default function() {  
    this.get('/todos', function(db, request) {
        return {
            data: db.todos.map(attrs => (
                {type: 'todos', id: attrs.id, attributes: attrs }
            ))
        };
    });
    this.post('/todos', function(db, request) {
        let attrs = JSON.parse(request.requestBody);
        let todo = db.todos.insert(attrs);
        return {
            data: {
                type: 'todos',
                id: todo.id,
                attributs: todo
            }
        };
    });
    this.patch('/todos/:id', function(db, request) {
        let attrs = JSON.parse(request.requestBody);
        let todo = db.todos.update(attrs.data.id, attrs.data.attributes);
        return {
            data: {
                type: "todos",
                id: todo.id,
                attributes: todo
            }
        };
    });
    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 create a factory which will generate the data.

$ touch app/mirage/factories/todo.js
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=newTitle enter="submitTodo"}}

Here we issue an action to our component 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({  
    actions: {
        submitTodo(newTitle) {
            if (newTitle) {
                this.sendAction('action', newTitle);
            }
            this.set('newTitle', '');
        }
    }
});

In this action we are checking that there is a newTitle and if there is we send the action out from the component. Then we clear the input with this.set('newTitle', '');. The action we send is the variable 'action'.

We must pass in this action variable when we implement the component on our app/templates/todos.hbs template, where we replace the input with our component.

{{todo-input action="createTodo"}}
{{#todo-list todos=model}}
    {{outlet}}
{{/todo-list}}

Now the sequence of events is: by hitting enter on the input box we run the action submitTodo, this action sends the action variable from the component. Sends it? Sends it where? Good question.

Time to introduce action bubbling. First our action is handled on the component, where it would, without help, die. This is because components are isolated. However we will help the action escape by sending it from the component to the route using sendAction. The todo-input component lives on the todos route, so this route gets first crack at handling the createTodo action. If you don't handle it on the todos route it will bubble to the application route. If you open up the inspector you can see the route tree. If none of your routes handle the action you get an error in the console like this.

We send the action away from the component because of the Data Down Actions Up principle. We send data down for example by sending the todo into the todo-item component. Here we send an action to create a new todo, up from the todo-input to one place (the router) that deals with maintaining that data.

Let's handle the action on app/routes/todos.js.

import Ember from 'ember';

export default Ember.Route.extend({  
    model() {
        return this.store.findAll('todo');
    },
    actions: {
        createTodo(newTitle) {
           this.store.createRecord('todo', {
               title: newTitle,
               complete: false
           }).save();
        }
    }
});

In our createTodo action, first we create an instance of a todo with the newTitle and default complete status of false. After that's done, we chain a save() on the end to send a request to our server to persist the data.

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 now use a 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
  • (TODO) it should be editable
  • (TODO) it should allow us to delete the 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}}. If you are familiar with if/else logic 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 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() {/** action **/},
    submitTodo() {
        let todo = this.get('todo');
        if (todo.get('title') === "") {
            this.sendAction('deleteTodo', todo);
        } else {
            this.sendAction('updateTodo', this.get('todo'));
        }
        this.set('editing', false);
    }
}   

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 or update it, sending the todo as a variable.

Now remember we need to update the template that has the todo-item on it, app/templates/todos/index.hbs, to pass in the two new action variables.

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

We need to handle these actions on our todos route, app/routes/todos.js.

import Ember from 'ember';

export default Ember.Route.extend({  
    model() {
        return this.store.findAll('todo');
    },
    actions: {
        createTodo(newTitle) {
            this.store.createRecord('todo', {
               title: newTitle,
               complete: false
            }).save();
        },
        updateTodo(todo) {
            todo.save();
        },
        deleteTodo(todo) {
            todo.destroyRecord();
        }
    }
});

In the updateTodo action we simply save the new todo, which will trigger a request to the server to persist the changes. In the deleteTodo action we call destroy record which deletes the record locally and sends a request to do the same on the server.

We've already written our action to delete todos, so 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() {
        let todo = this.get('todo');
        this.sendAction('deleteTodo', todo);
    }
}

This will work because we already have an action on our todos route to deal with deleting a todo.

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() {
        return this.store.filter('todo', function(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 this.store.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 be handled by our todo-list component.

First lets change the hardcoded value of '1' to a property of the component {{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 send the deleteTodo action from the component forEach todo to be handled on the todos route.

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}}

We also send an action of deleteTodo from the component so we need to pass in this variable on the todos template, app/templates/todos.hbs.

{{todo-input action="createTodo"}}
{{#todo-list todos=model deleteTodo="deleteTodo"}}
    {{outlet}}
{{/todo-list}}

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}}
</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);
        }
    },
    allAreDoneObserver: Ember.observer('allAreDone', function() {
        let completeValue = this.get('allAreDone');
        let todos = this.get('todos');
        todos.forEach((todo) => {
            todo.set('complete', completeValue)
            this.sendAction('updateTodo', todo);
        });
    }),
    actions: { /* truncated for brevity */ }
});

Here we use the didInsertElement() hook, which is called each time the component is rendered or re-rendered (on a data change). 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.

allAreDoneObserver is an ember observer, it watches the value, allAreDone, and when it changes runs the function supplied. In the function we go through each todo and set the complete property to match the allAreDone property.

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.

comments powered by Disqus