Ok, I’m convinced… Ember-cli is Awesome! Now, how do I use it? (Part 2)

Our app works now… sort of… but we still need to be able to check off each todo item as it’s completed, make changes to our todo’s, and eliminate our old todo’s with extreme prejudice.

This thing is pretty ugly, I’ll admit–even for a nerd. If you want to take a detour and Bootstrap, Compass, Foundation, or otherwise spiffy up your app, feel free. Details for how to get started doing this can be found at Ember-cli’s asset compilation page here.

As you might guess, we import the necessary assets in the ember-cli-build.js file. If we just wanted to add our own custom css changes, we could do so in app/styles/app.css.

Done yet? Good! Let’s continue.

Let’s add an action to our checkboxes in our todos template and a handler to our ‘todos’ route so we can check off our todos We will also need to add a “checked” attribute to our todo model.

In app/templates/todos.js:

1 <h1>Todos</h1>
2 <div class="nav">{{#link-to "new"}}New Todo{{/link-to}}</div>
3 {{#each model as |todo|}} 
4 {{#if todo.checked }} <label class="strikethrough"><input checked="checked" type="checkbox" />{{#link-to "todo" todo}}{{todo.name}}{{/link-to}}</label>
5 {{else}} 
6 <label><input type="checkbox" />{{#link-to "todo" todo}}{{todo.name}}{{/link-to}}</label>
7 {{/if}} 
8 {{/each}}

In app/routes/todos.hbs:

  
  1 import Ember from 'ember';                                                                                         
  2                                                                                                                    
  3 export default Ember.Route.extend({                                                                                
  4         model: function () {                                                                                       
  5                 return this.store.findAll('todo');                                                                 
  6         },                                                                                                         
  7        actions: {                                                                                                  
  8                                                                                                                    
  9                checkOff: function (todo) {                                                                         
 10                        todo.toggleProperty('checked');                                                             
 11                        todo.save();                                                                                                                                                                                                       
 12                }                                                                                                   
 13        }                                                                                                           
 14 }); 

In app/models/todo.js:

  1 import DS from 'ember-data';                                                                                                                                                                                                              
  2 
  3 export default DS.Model.extend({
  4        name: DS.attr('string'),
  5        details: DS.attr('string'),
  6        checked: DS.attr('boolean')
  7   
  8 });

Alrighty. Good bit going on here. We have a handlebars if/else block that changes what is displayed based on the truthiness of our model’s checked attribute. There is probably a better way to do that, but finding a more D.R.Y. way to do this is something you can do as a homework exercise. If our todo is checked off, we want the todos route to display a checked checkbox. I also added the ‘strikethrough’ class to the label to add the line-through text decoration. In my app.css, my strikethrough class looks like this:

.strikethrough {
    text-decoration: line-through;
}

Now, we need to handle editing our todos. We can do this in a separate edit route, or we can go ahead and do it on the todo route. There are probably still more ways we could implement this, but those are the two that come to mind. Let’s go ahead and add our edit feature. We will use an edit button with an ‘edit’ action. We will then redirect to the ‘edit’ route, reusing the ‘new’ template after we make a few minor changes to it. We could have the ‘edit’ action called when the text is clicked on, but then we would need to provide some sort of indication that clicking on the text would allow you to edit it, probably in the form of a :hover CSS rule. You can do whatever you want. We will start in app/templates/todo.hbs:


1<h1>{{model.name}}</h1>
2<pre>{{model.details}}</pre>
3 <button {{action 'edit' model}}>Edit</button>

Here, we pass the model to the edit action, and we’ll see why when we get there. But first, we need to create our edit route:

ember g route edit

Now, let’s edit the edit route (app/routes/edit.js):

  1 import Ember from 'ember';
  2 
  3 export default Ember.Route.extend({
  4         renderTemplate: function () {
  5                 this.render('new');
  6         },
  7         actions: {
  8 
  9                 save: function (todo) {
 10                        var name = $('input').val();
 11                        var details = $('textarea').val();
 12                        var model = this.get('model');
 13                        todo.set('name', name);
 14                        todo.set('details', details);
 15                        todo.save();
 16                        this.transitionTo('todo', todo);                                                                                                                                                                                   
 17                 }
 18         }
 19 });

Notice renderTemplate in the above route. Our ‘edit’ route will reuse our ‘new’ template, with a few minor but important changes. Take a look (app/templates/new.hbs):

1 <h1>Todo</h1>
2 
3 <input type="text" value="{{model.name}}" placeholder="Todo Name" /> 
4 <hr />
5 <textarea placeholder="Todo Details">{{model.details}}</textarea> 
6 <hr />
7 <button>Done</button>

We are passing the model to the save action now. This will not affect how our ‘new’ route handles the save action since it did not receive a model in the first place.

Congrats! We can now edit our todos!

Now, let’s handle deleting checked-off todos. We need to start by editing the todos template and adding a delete checked button with an appropriately-named action handler.

In app/templates/todos.hbs:

  1 <h1>Todos</h1>                                                                                                                                                                                                                            
  2 <div class="nav">{{#link-to "new"}}New Todo{{/link-to}}</div>
  3 {{#each model as |todo|}}
  4 {{#if todo.checked }} <label {{action "checkOff" todo}} class="strikethrough"><input type="checkbox" checked>{{#link-to "todo" todo}}{{todo.name}}{{/link-to}}</label><br>
  5 {{else}}
  6 <label {{action "checkOff" todo}}><input type="checkbox">{{#link-to "todo" todo}}{{todo.name}}{{/link-to}}</label><br>
  7 {{/if}}
  8 {{/each}}
  9 <button {{action 'deleteTodos'}}>Delete checked todos</button>

Nothing we haven’t seen before here. We have a button with an action handler that currently doesn’t have an action to match it. All we have left to do is add another action and we are done. We will get all the todos and loop through them using forEach. If their checked attribute is true, we will delete them (app/routes/todos.js):

  1 import Ember from 'ember';                                                                                                                                                                                                                
  2 
  3 export default Ember.Route.extend({                                                                                
  4         model: function () {
  5                 return this.store.findAll('todo');
  6         },
  7        actions: {
  8                checkOff: function (todo) {                                                                         
  9                        todo.toggleProperty('checked');
 10                        todo.save();
 11                },
 12        deleteTodos: function () {
 13                this.store.find('todo').then(function (items) {
 14                        console.log(items);
 15                        items.forEach(function (item) {
 16                                if(item.get('checked')) {
 17                                        item.deleteRecord();
 18                                        item.save();
 19                                }
 20                        });
 21                });
 22        }
 23        }
 24 });

That’s it! We are finished! Congratulations! You are now an Ember master. Go forth and use your powers for good. There are plenty of improvements we could (read: probably should) make to this example, but I will leave that to you. Good luck, have fun, and feel free to ask for help when you need it. If you are stuck, you can get the source code here.

Ok, I’m convinced… Ember-cli is Awesome! Now how do I use it? (Part 1)

I thought you would never ask…

Getting into Ember-cli is easier than one might expect, especially for those who are comfortable with Rails, and are comfortable working in a Unix-based terminal. First, you need to install it, and in order to install it, we will need node and npm. My environment is Linux, so these instructions are heavily biased in that direction. We will need to start off by running these commands as root in the terminal:

apt-get install node
apt-get install npm

Now, the real fun begins. Using the Node Package Manager (npm) that we just installed, we can install Ember-cli:

npm install -g ember-cli

If we wanted Bower (we do, by the way), we could install it too.

npm install -g bower

That should be enough to get started. There are more detailed instructions on how to install Ember-cli available on the user guide here.

Next, we will want to create a new project. Navigate in the terminal to the directory we would like to create our new project in (in my case, it’s ~/MyProjects), and let Ember-cli do all the boilerplating for us:

ember new my-new-app

Navigating into the my-new-app/ directory should reveal a set of directories and a few files. The app/ directory is where we will be spending most, if not all of our time. Let’s go ahead and make sure everything is good to go. Enter the command:

ember serve

Navigating to the URL (by default, it’s http://localhost:4200) that we are directed to in a web browser should show us a “Welcome to Ember” message if everything is working properly. We could stop the server by hitting ctrl+C in the terminal, but let’s just leave it running and open another terminal instead. Hitting File > Open Terminal in the terminal window(or shift+ctrl+N) will open a new terminal in our current working directory.

Now, let’s go ahead and create a resource. Let’s make an awesome and innovative app… why not a todo-list app? I’m sure no one has thought of doing that before! Ember makes this super-easy:

ember g resource todo

The ‘g’ in the previous command stands for generate. We could of course type it out, but why bother with those seven extra keystrokes? There are a few other minor things we need to take care of before we can really get our hands dirty. We need a ‘todos’ route so that we can see all of our todos at once:

ember g route todos

Once we have everything set up properly, we will then be able to go to localhost:4200/todos (well, localhost:4200 once we set the path, but I’m getting ahead of myself) and see all the todos that we have created. Let’s go ahead and create a ‘new’ route as well.

ember g route new

Now, we can get our hands dirty. In app/models/todo.js, let’s go ahead and set up our todo model. We can always go back later and add/remove things as necessary.

  1 import DS from 'ember-data';
  2                                                                             
  3 export default DS.Model.extend({
  4        name: DS.attr('string'),
  5        details: DS.attr('string')
  6   
  7 });

Great job! That should do for now. While we don’t ‘have’ to use Ember Data, it’s pretty sweet. The next step is going to involve router.js. This file will be in the app/ directory. The router handles URL’s for our app. We will want to set up a dynamic route for ‘todo.’ To do this, all we have to do is pass in an id for the todo we want in our path. We also want our base URL to be the ‘todos’ route. We will take care of this in a similar manner–by specifying a path:

  1 import Ember from 'ember';
  2 import config from './config/environment';
  3 
  4 var Router = Ember.Router.extend({
  5   location: config.locationType
  6 });
  7                                                                             
  8 Router.map(function() {
  9   //The only lines we change are the two below
 10   this.route('todo', {path: 'todo/:todo_id'});
 11   this.route('todos', {path: '/'});
 12   this.route('new');
 13 });
 14 
 15 export default Router;

Alright. Now, we’re getting somewhere! We’re still a ways away from a working app, though. At this point, we could stop and get the front-end looking acceptable and add a few link-to helpers so everything is interconnected. There would be nothing wrong with that, and it can actually improve our workflow by helping us eliminate cruft from our app. However, since it’s just us and this is a pretty simple example, let’s just skip that until later. Right now, all we have are routes and a model. Unless we want to roll our own adapter or use the default REST adapter, we will need to install an adapter. In our terminal, we need to run:

bower install --save-dev ember-localstorage-adapter

The localstorage adapter will allow our Ember app to make requests for data and save data to our web browser’s localstorage. The setup isn’t too bad. First, in ember-cli-build.js, we need to add an app.import call for the adapter:

  1 /* global require, module */
  2 var EmberApp = require('ember-cli/lib/broccoli/ember-app');
  3 
  4 module.exports = function(defaults) {
  5   var app = new EmberApp(defaults, {
  6     // Add options here
  7   });
  8   app.import('bower_components/ember-localstorage-adapter/localstorage_adapter.js');                                                                                                                                                      
  9 
 10   // Use `app.import` to add additional libraries to the generated
 11   // output files.
 12   //
 13   // If you need to use different assets in different
 14   // environments, specify an object as the first parameter. That
 15   // object's keys should be the environment name and the values
 16   // should be the asset to use in that environment.
 17   //
 18   // If the library that you are including contains AMD or ES6
 19   // modules that you would like to import into your application
 20   // please specify an object with the list of modules as keys
 21   // along with the exports of each module as its value.
 22 
 23   return app.toTree();
 24 };

Next, we have to generate an adapter using the ember generator, and then add a namespace to it.

In the terminal:

ember g adapter application

In app/adapters/application.js:

  1 import DS from 'ember-data';
  2 
  3 export default DS.RESTAdapter.extend({
  4         namespace: 'my-new-app'                                    
  5 });

Our adapter is now ready and willing to serve up data for us. We now need to make some templates for that data to go into, and a few ways for the user to interact with said data. Ember leverages the handlebars templating language, so you will see a lot of code in {{curly braces}}. You will also notice that the file extensions for our templates are *.hbs and not *.html. You may notice that syntax highlighting doesn’t work for .hbs files in your text editor of choice. Since we are using Vim as our text editor (ok, you don’t have to use Vim… but if you haven’t, consider this your final warning… It’s addictive), we will probably want to add a line to our .vimrc file to beautify our code a little. This is a workaround, not an ideal fix:

au BufReadPost *.hbs set syntax=html

In app/templates, we see a listing of our templates. Let’s make a few changes to our todos.hbs file. Nothing too fancy. We are all nerds here after all…

  1 <h1>Todos</h1>
  2 <div class="nav">{{#link-to "new"}}New Todo{{/link-to}}</div>                                                                                                                                                                             
  3 {{#each model as |todo|}}
  4 <label><input type="checkbox">{{#link-to "todo" todo}}{{todo.name}}{{/link-to}}</label><br>
  5 {{/each}}

The site should automagically reload after these changes are saved. ‘Hey, where’s mah data?’ Well, there is none… yet. We could add a model hook to our todos route with some dummy data, or add a test that throws in some dummy data just to make sure everything works, but I’m going to skip that for the sake of brevity. Before we move any further, we need to understand what’s going on here. The {{#each}} handlebars helper allows us to loop through the data passed to our route and do awesome stuff with said data. Pretty nifty, huh? The {{#link-to}} helper allows us to add links to our other routes. Since our ‘todo’ route is a dynamic route, we need to specify which todo the link goes to by passing in a todo. Ember will pull the ID from that todo and pass it through whenever we request that route. We need to knock out a few more things before things start working the way one would expect:

(1) add a model hook to our ‘todos’ route
(2) finish our new.hbs template
(3) add an action handler to our ‘new’ route
(4) edit the todo route.

Alright. Let’s start with app/routes/todos.js:

  1 import Ember from 'ember';
  2 
  3 export default Ember.Route.extend({
  4         model: function () {
  5                 return this.store.findAll('todo');
  6         }                                                                                                                                                                                                                                 
  7 });

That was pretty straightforward. All we need to do in the ‘todos’ model hook is find all the todo’s and return them. Now, let’s finish our new.hbs template. The full path from within our project directory is app/templates/new.hbs:

  1 <h1>New Todo</h1>                                                                                                                                                                                                                         
  2                                                                                                                    
  3 <input type="text" placeholder="Todo Name">
  4 <hr>   
  5 <textarea placeholder="Todo Details"></textarea>
  6 <hr>
  7 <button {{action "save"}}>Create Todo</button>

In the button, we see an action handler called “save.”

If we go to our ‘new’ route within our app now, we can’t create a new todo. In our console, we would see an error message stating that nothing handled the action “save” after we click the button. Let’s fix that. We need to open up app/routes/new.js in a text editor.

  1 import Ember from 'ember';                                                                                                                                                                                                                
  2                                                                                                                    
  3 export default Ember.Route.extend({
  4        actions: {                                                                                                  
  5                save: function () {
  6                        var newtodo = this.store.createRecord('todo',
  7                                {
  8                                        name: $("input").val(),
  9                                        details: $("textarea").val()
 10                                });
 11                        newtodo.save();
 12                        console.log("model saved");
 13                }
 14        }
 15 });

Now, the ‘save’ action creates a new todo for us. We could even make a slight improvement to this. Suppose we wanted the app to redirect us to the ‘todos’ route after a new todo was created. All we need for that is a single line added to the end of our action handler: this.transitionToRoute(‘todos’);

It’s not necessary by any means, but it could be a pretty nice UX feature if our users typically only add one todo at a time. Another addition that may make sense is adding an alert message or some other form of feedback to the user when the new todo is created. It could even be as simple as clearing the input and textarea fields, and focusing on the input field. In fact, that makes a lot of sense. Let’s go ahead and implement that:

  1 import Ember from 'ember';                                                                                         
  2                                                                                                                    
  3 export default Ember.Route.extend({                                                                                
  4        actions: {                                                                                                  
  5                save: function () {                                                                                 
  6                        var newtodo = this.store.createRecord('todo',                                               
  7                                {                                                                                   
  8                                        name: $("input").val(),                                                     
  9                                        details: $("textarea").val()                                                
 10                                });                                                                                 
 11                        newtodo.save();                                                                             
 12                        $("textarea").val("");                                                                      
 13                        $("input").val("").focus();                                                                                                                                                                                        
 14                                                                                                                    
 15                }                                                                                                   
 16        }                                                                                                           
 17 });  

That’s a lot better! Now, let’s go ahead and change our todo route and template so we can view each todo we created and read the details from them:

app/templates/todo.js:

  1 <h1>{{model.name}}</h1>
  2 
  3 <h2>Details</h2>                                                                                                                                                                                                                          
  4 <p>{{model.details}}</p>

app/routes/todo.js:

  1 import Ember from 'ember';
  2 
  3 export default Ember.Route.extend({                                                                                                                                                                                                       
  4         model: function (params) {
  5                 this.store.find('todo', params.todo_id);
  6         }
  7 });

Yippie! Once we create a few todos, we can view them! There’s our details, and the name of the todo. We are a lot closer now. Still, there’s the ‘UD’ in our CRUD app to knock out.

This is a good stopping point for now. Next time, we will go over a few possibilities for editing and deleting todos, and implement that functionality. If you would like to access the source code for this project, I will have it up on Github hopefully before the end of the week, along with the second half of this brief introduction to Ember.