Ember CLI - TodoMVC Tutorial (for v0.0.47)
This post will walk you through how to build the Todo MVC app that the Official Ember Getting Started Guide walks you through but shows you how to use Ember CLI instead.
Using Ember CLI has many benefits over just linking to ember in your html script tag, here are a few:
- Comes with a built in webserver
- Auto builds your code so you don’t have to refresh the browser all the time (there might be a bug in 0.0.47 that is keeping this from working)
- Default folder structure so every project looks the similar
- Can generate model, controller, template, route, and test files for you
- You can write ES6 javascript
Because ember-cli is so helpfull it is going to be part of ember 2.0 so I think it is very beneficial to start getting familiar with it now.
Getting Started
The first thing we need to do is install Ember CLI. Here are the basic steps for getting it installed on your system. For more details please see the official website. This guide was written for Ember CLI version 0.0.47. Other versions might work but your mileage may vary. Anyways, back to installing Ember CLI
At your terminal type:
npm install -g ember-cli
And now install Bower:
npm install -g bower
Now navigate to where you want to place your project code and create a new ember app:
ember new todos
Now start the server:
cd todos
ember server
Navigate to http://localhost:4200
in your web browser to see your new app in action. I recommend using Chrome or Firefox because they have Ember Inspector, but also because I feel like those browsers have better tooling for viewing javascript errors.
Inside of your browser you should see simple page that says: “Welcome to Ember.js”. On this page right-click and select ‘Inspect Element’ so that you can see if any javascript errors occur while we are building our app.
I like to leave the server running in my terminal, since it will automatically build any changes you make. So create a new terminal tab/window and stay inside the todos directory so that we can run some ember commands.
Planning The Application
There are no differences in this section
Creating a Static Mockup
Replace the text inside of app/templates/application.hbs
with the following:
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<input type="text" id="new-todo" placeholder="What needs to be done?" />
</header>
<section id="main">
<ul id="todo-list">
<li class="completed">
<input type="checkbox" class="toggle">
<label>Learn Ember.js</label><button class="destroy"></button>
</li>
<li>
<input type="checkbox" class="toggle">
<label>...</label><button class="destroy"></button>
</li>
<li>
<input type="checkbox" class="toggle">
<label>Profit!</label><button class="destroy"></button>
</li>
</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>
</section>
<footer id="info">
<p>Double-click to edit a todo</p>
</footer>
Replace the contents of app/styles/app.css
with this stylesheet.
Inside of the public/
folder create a new folder called assets
and place the bg.png
file in that folder.
Obtaining Ember.js and Dependencies
We’ve already obtained Ember.js and dependencies! Move on to the next section.
Adding the First Route and Template
Inside app/router.js
file add this line to the Router.map
body:
this.resource('todos', { path: '/' });
The app/router.js
file should look like this now:
import Ember from 'ember';
import config from './config/environment';
var Router = Ember.Router.extend({
location: config.locationType
});
Router.map(function() {
this.resource('todos', { path: '/' });
});
export default Router;
From the terminal type:
ember generate route todos
This will create a new file for us inside of app/templates/todos.hbs
. Now copy the entire contents of the app/templates/application.hbs
file into this new file.
Then replace the entire contents of the app/templates/application.hbs
file with:
Now go back to your web browser and make sure everything still loaded okay.
Modeling Data
From the terminal type:
ember generate model todo
Now open up js/models/todo.js
and add the title
and isCompleted
attributes:
import DS from 'ember-data';
export default DS.Model.extend({
title: DS.attr('string'),
isCompleted: DS.attr('boolean')
});
Using Fixtures
To enable fixtures run the command:
ember generate adapter application
and inside the app/adapters/application.js
file replace RESTAdapter
with FixtureAdapter
.
import DS from 'ember-data';
export default DS.FixtureAdapter.extend();
Now go back to our model app/models/todo.js
and add a fixture to it. Also, make sure to create the Todo
variable. The file should now look like:
import DS from 'ember-data';
var Todo = DS.Model.extend({
title: DS.attr('string'),
isCompleted: DS.attr('boolean')
});
Todo.reopenClass({
FIXTURES: [
{
id: "1",
title: 'install ember-cli',
isCompleted: true
}, {
id: "2",
title: 'install additional dependencies',
isCompleted: true
}, {
id: "3",
title: 'develop amazing things',
isCompleted: false
}
]});
export default Todo;
Displaying Model Data
Inside of app/routes/todos.js
and place the following code:
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
return this.store.find('todo');
}
});
Now we need to add some handlebar code to our app/templates/todos.hbs
template so that it can dynamically load the contents of our fixture.
Displaying a Model’s Complete State
Update the <li>
tag in app/templates/todos.hbs
:
Creating a New Model Instance
At the top of app/templates/todos.hbs
replace the new todo <input>
with an {{input}}
helper:
Now we need to create a new controller for our todos so that we can something to handle our actions:
ember generate controller todos --type=array
This will generate a new file for us in app/controllers/todos.js
. Edit this file so that it now looks like:
import Ember from 'ember';
export default Ember.ArrayController.extend({
actions: {
createTodo: function() {
// Get the todo title set by the "New Todo" text field
var title = this.get('newTitle');
if (!title.trim()) { return; }
// Create the new Todo Model
var todo = this.store.createRecord('todo', {
title: title,
isCompleted: false
});
// Clear the "New Todo" text field
this.set('newTitle', '');
// Save the new model
todo.save();
}
}
});
Marking a Model as Complete or Incomplete
Inside the app/templates/todos.hbs
template replace the <input>
tag for the checkbox with:
Now lets create another controller:
ember generate controller todo --type=object
Inside of the app/controllers/todo.js
file place the following code:
import Ember from 'ember';
export default Ember.ObjectController.extend({
isCompleted: function(key, value){
var model = this.get('model');
if (value === undefined) {
// property being used as a getter
return model.get('isCompleted');
} else {
// property being used as a setter
model.set('isCompleted', value);
model.save();
return value;
}
}.property('model.isCompleted')
});
Displaying the Number of Incomplete Todos
Update the static count inside the todo-count
span in the app/templates/todos.hbs
template:
Add this to the app/controllers/todos.js
file:
// ... additional lines truncated for brevity ...
actions: {
// ... additional lines truncated for brevity ...
},
remaining: function() {
return this.filterBy('isCompleted', false).get('length');
}.property('@each.isCompleted'),
inflection: function() {
var remaining = this.get('remaining');
return remaining === 1 ? 'todo' : 'todos';
}.property('remaining')
// ... additional lines truncated for brevity ...
Toggling between Showing and Editing States
Update app/templates/todos.hbs
with the following code to enable toggling in and out of edit mode:
And then update our app/controllers/todo.js
controller:
// ... additional lines truncated for brevity ...
export default Ember.ObjectController.extend({
actions: {
editTodo: function() {
this.set('isEditing', true);
}
},
isEditing: false,
isCompleted: function(key, value){
// ... additional lines truncated for brevity ...
You can now double-click a todo to edit it.
Accepting Edits
In app/templates/todos.hbs
change out the static input tag under if editing:
In app/controllers/todo.js
add the acceptChanges
action:
// ... additional lines truncated for brevity ...
actions: {
editTodo: function() {
this.set('isEditing', true);
},
acceptChanges: function() {
this.set('isEditing', false);
if (Ember.isEmpty(this.get('model.title'))) {
this.send('removeTodo');
} else {
this.get('model').save();
}
}
},
// ... additional lines truncated for brevity ...
Deleting a Model
In app/templates/todos.hbs
update the delete button:
In app/controllers/todo.js
add the removeTodo
action:
// ... additional lines truncated for brevity ...
actions: {
editTodo: function() {
this.set('isEditing', true);
},
acceptChanges: function() {
this.set('isEditing', false);
if (Ember.isEmpty(this.get('model.title'))) {
this.send('removeTodo');
} else {
this.get('model').save();
}
},
removeTodo: function() {
var todo = this.get('model');
todo.deleteRecord();
todo.save();
}
},
// ... additional lines truncated for brevity ...
Now you can delete todos.
Adding Child Routes
Create a new template for our child route:
ember generate route todos/index
Inside of app/templates/todos.hbs
move the entire contents of <ul id="todo-list"
section (including the <ul></ul>
tags) into this new file: app/templates/todos/index.hbs
.
Within app/templates/todos.hbs
place a Handlebars {{outlet}}
helper where the <ul>
was previously:
In app/router.js
change the todos
mapping with an additional empty function parameter so it can accept child routes:
Router.map(function() {
this.resource('todos', { path: '/' }, function() {
// additional child routes will go here later
});
});
Add the following code to app/routes/todos/index.js
:
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
return this.modelFor('todos');
}
});
Transitioning to Show Only Incomplete Todos
In app/templates/todos.hbs
convert the <a>
tag for ‘Active’ todos into a Handlebars `` helper:
Let’s generate a our ‘active’ route:
ember generate route todos/active
In app/router.js
update the router add the active route under the ‘todos’ resource and be sure to clean up the ‘todos/active’ route created by the generator:
import Ember from 'ember';
import config from './config/environment';
var Router = Ember.Router.extend({
location: config.locationType
});
Router.map(function() {
this.resource('todos', { path: '/' }, function() {
this.route('active');
});
});
export default Router;
In app/routes/todos/active.js
implement a matching route:
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
return this.store.filter('todo', function(todo) {
return !todo.get('isCompleted');
});
},
renderTemplate: function(controller) {
this.render('todos/index', { controller: controller} );
}
});
Transitioning to Show Only Complete Todos
In app/templates/todos.hbs
convert the <a>
tag for ‘Completed’ todos into a Handlebars `` helper:
Let’s generate a new route for showing the completed tasks with:
ember generate route todos/completed
In app/router.js
update the router add the ‘completed’ route under the ‘todos’ resource and be sure to clean up the ‘todos/completed’ route created by the generator:
Router.map(function() {
this.resource('todos', { path: '/' }, function() {
this.route('active');
this.route('completed');
});
});
In app/routes/todos/completed.js
implement a matching route:
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
return this.store.filter('todo', function(todo) {
return todo.get('isCompleted');
});
},
renderTemplate: function(controller) {
this.render('todos/index', {controller: controller});
}
});
Transitioning back to Show All Todos
In app/templates/todos.hbs
convert the <a>
tag for ‘All’ todos into a Handlebars {{link-to}}
helper:
Displaying a Button to Remove All Completed Todos
In app/templates/todos.hbs
update the static <button>
for clearing all completed todos:
In app/controllers/todos.js
implement the matching properties and a method that will clear completed todos and persist these changes when the button is clicked:
import Ember from 'ember';
export default Ember.ArrayController.extend({
actions: {
clearCompleted: function() {
var completed = this.filterBy('isCompleted', true);
completed.invoke('deleteRecord');
completed.invoke('save');
},
// ... additional lines truncated for brevity ...
},
hasCompleted: function() {
return this.get('completed') > 0;
}.property('completed'),
completed: function() {
return this.filterBy('isCompleted', true).get('length');
}.property('@each.isCompleted'),
// ... additional lines truncated for brevity ...
Indicating When All Todos Are Complete
In app/templates/todos.hbs
replace the static checkbox <input>
with an {{input}}
:
In app/controllers/todos.js
implement the matching allAreDone
property:
// ... additional lines truncated for brevity ...
allAreDone: function(key, value) {
return !!this.get('length') && this.isEvery('isCompleted');
}.property('@each.isCompleted')
// ... additional lines truncated for brevity ...
Toggling All Todos Between Complete and Incomplete
Update the allAreDone
property in app/controllers/todos.js
to handle both getting and setting behavior:
// ... additional lines truncated for brevity ...
allAreDone: function(key, value) {
if (value === undefined) {
return !!this.get('length') && this.everyProperty('isCompleted', true);
} else {
this.setEach('isCompleted', value);
this.invoke('save');
return value;
}
}.property('@each.isCompleted')
// ... additional lines truncated for brevity ...
Replacing the Fixture Adapter with Another Adapter
I couldn’t figure out how to get this to work with Ember CLI, but I will be writing another post that talks about how to connect to a Rails backend for data persistance.