A Restless Todo app with EmberJS and Flask: Part 2

This is Part 2 of a tutorial aimed demonstrating how to build a Flask-based EmberJS application. If you haven’t read Part 1, you’ve missed the part where we build a Python backend.

For our initial Todo application we are going to use the great code provided by TodoMVC project. To kick everything off we are going to throw our app into strict mode. A great explanation is provided by John Resig. It boils down to disabling eval and with and prevent assigning values to variables which have not been defined yet.

Create the application

Let’s go ahead and create an Ember application:

var Todos = Ember.Application.create({
    LOG_TRANSITIONS: true
});

LOG_TRANSITIONS logs to the console when the application transitions to a new route. The default is is false if you don’t want to see route transitions in your browser log.

Make sure Ember knows where to send data

In the previous tutorial, we created a Flask-based data store.

Todos.Store = DS.Store.extend({
    revision: 12,
    adapter: DS.RESTAdapter.create({
        namespace: 'api'
    })
});

We have to specify a revision number so we are notified of any ember-data API changes. By default the RESTAdapter assumes the API endpoint to exist on the same domain as itself. For example if we are hosting our app at example.com, the RESTAdapter will look for API endpoints at ‘http://example.com/api/

Create a model

Todos.Todo = DS.Model.extend({
    title: DS.attr('string'),
    isCompleted: DS.attr('boolean'),

    todoDidChange: function () {
        Ember.run.once(this, function () {
            this.get('store').commit();
        });
    }.observes('isCompleted', 'title')
});

This where we set up a model to reflect our Todo object. This should look very similar to how we set up our Todo model with SQLAlchemy in todo.py. Missing is the id attribute. This is assumed to be ‘id’ by ember-data.

Ember has the default attribute types of string, number, boolean, and date.

The last thing going on here is an inline observer. It’s best to read this code starting from the bottom. The both isCompleted and title attributes are observed for changes and if they are changed the todoDidChange method is called which happens to commit the changed model to the data store. This is PUT request to ‘/api/todos/' in our case.

Views (which I think are confusing)

Quick description of the purpose of views: The purpose of a view is to translate primitive browser events into events that have meaning to your application. So we define how we are going to display each Todo model in the template and what will happen if one of the models is double clicked:

Todos.TodoView = Ember.View.extend({
    tagName: 'li',
    classNameBindings: [
        'todo.isCompleted:completed',
        'isEditing:editing'
    ],
    doubleClick: function () {
        this.set('isEditing', true);
    }
});

In our Handlebars template the view representing a Todo model looks like this:

<li {{bindAttr class="isCompleted:completed isEditing:editing"}}>
{{#if isEditing}}
    {{view Todos.EditTodoView todoBinding="this"}}
{{else}}
    {{view Ember.Checkbox checkedBinding="isCompleted" class="toggle"}}
    <label {{action "editTodo" on="doubleClick"}}>{{title}}</label>
    <button {{action "removeTodo"}} class="destroy"></button>
{{/if}}
</li>

The default view presentation in the DOM is a <div> but this overrided with tagName: 'li'. Next up we bind the object properties to CSS classes. So if sometodo.isCompleted = true then we would have <li class="completed">. This is just to determine how the Todo should be displayed. The second class binding is determined by a translation of a browser event, doubleClick on the TodoView. When an <li> generated by our view is double clicked its isEditing property is set to true and because we set the binding above, the class ‘editing’ is added to the view.

A Todo model can have a state where it is being edited. We need to create view for this so we can manipulate it.

Todos.EditTodoView = Ember.TextField.extend({
    classNames: ['edit'],

    valueBinding: 'todo.title',

    change: function () {
        var value = this.get('value');

        if (Ember.isEmpty(value)) {
            this.get('controller').removeTodo();
        }
    },

    focusOut: function () {
        this.set('controller.isEditing', false);
    },

    insertNewline: function () {
        this.set('controller.isEditing', false);
    },

    didInsertElement: function () {
        this.$().focus();
    }
});

This view is triggered when the <li>is double clicked. The EditTodoView extends a built in view called TextField.

classNames: ['edit'] just sets the class names of the outer element of the view. The default is ‘ember-view’

valueBinding: 'todo.title' the value of the TextField view is bound to * the title attribute of the individual EditTodoView.

The rest of the methods are DOM events that are similar to those in jQuery. One thing to note change fires every time the value of the TextField changes, so by this nature it creates a lot of PUT requests on the server.

Rooting for a router

Routing helps make the URLs of your application meaningful. For example, ‘http://example.com/#/active’ means that we would want to see all the todos which are not completed. This URL could be reached by the user interacting with the view (i.e. clicking on the ‘Active’ link) or the user could type into the URL bar or have it booked mark and it still results in your application being in the same state: showing only todos which are not complete.

When our application is started the router is responsible for examining the URL and loading the appropriate data and views for that state.

Below is an example of grouping routes which will work together. Since there is only one resource we are working with, todos, we make sure the todos are loaded at the index of our app, ‘http://example.com/’. This will also create three routes. ‘/’, ‘/active’ and ‘/complete’ will all load and display different data.

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

If a route does not have a dynamic segment, you can hardcode which model should be associated with that URL by implementing the route handler’s model hook. That means we won’t use a URL like ‘/todos/1/edit’

Todos.TodosRoute = Ember.Route.extend({
    model: function () {
        return Todos.Todo.find();
    }
});

Templates are usually only useful if they have information to display and so we have to make sure that the controller has the information it needs. As we setup above, the index route, ‘/’ will provide data for the controller. It’s important to remember that controllers are created if they don’t exist. So for TodosIndexRoute a TodosIndexController was created. So we * need provide this controller with some data. In this case, we make available filteredTodos which is provided by the array todos

Todos.TodosIndexRoute = Ember.Route.extend({
    setupController: function () {
        var todos = Todos.Todo.find();
        this.controllerFor('todos').set('filteredTodos', todos);
    }
});

This route works like the above, but is found at ‘/active’ and the array todos only contains Todo objects in which isCompleted is false.

Todos.TodosActiveRoute = Ember.Route.extend({
    setupController: function () {
        var todos = Todos.Todo.filter(function (todo) {
            if (!todo.get('isCompleted')) {
                return true;
            }
        });

        this.controllerFor('todos').set('filteredTodos', todos);
    }
});

The route ‘/completed’ is just the inverse of the above.

Todos.TodosCompletedRoute = Ember.Route.extend({
    setupController: function () {
        var todos = Todos.Todo.filter(function (todo) {
            if (todo.get('isCompleted')) {
                return true;
            }
        });

        this.controllerFor('todos').set('filteredTodos', todos);
    }
});

Controllers

Our template translates the actions in the browser to semantic events for our applications, but we then must decide what to do with those events. This where controllers enter the picture.

A template gets its properties from its controller. In this case the todos template will use the TodosController to interact with the Todo model.

First up is a createTodo action which is called from the template at line 14. The TextField view binds its value to newTitle and createTodo takes this value and creates a new Todo record wit hit and then resets the view.

Todos.TodosController = Ember.ArrayController.extend({
    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
        Todos.Todo.createRecord({
            title: title,
            isCompleted: false
        });

        // Clear the "New Todo" text field
        this.set('newTitle', '');

        // Save the new model
        this.get('store').commit();
    },

    /*
     * This should be self-explanatory. We have seen something similar in 
     * the routes above but this time there is a builtin filterProperty 
     * method. Which will return array of Todo objects. Then `invoke` calls
     * the `deleteRecord` method on each object that implements it.
     * After that changes are commited.
     */
    clearCompleted: function () {
        var completed = this.filterProperty('isCompleted', true);
        completed.invoke('deleteRecord');

        this.get('store').commit();
    },

    /*
     * Pretty clear, but the fun happens with
     * `.property('@each.isCompleted')`. This instructs Ember.js to update
     * bindings and fire observers for this computed property. So if a Todo
     * is added, removed or updated `remaining` will return a value that
     * reflects this
     */
    remaining: function () {
        return this.filterProperty('isCompleted', false).get('length');
    }.property('@each.isCompleted'),

    /*
     * When the `remaining` computed property changes so does the
     * `remainingFormatted` computed property.
     */
    remainingFormatted: function () {
        var remaining = this.get('remaining');
        var plural = remaining === 1 ? 'item' : 'items';
        return '<strong>%@</strong> %@ left'.fmt(remaining, plural);
    }.property('remaining'),

    completed: function () {
        return this.filterProperty('isCompleted', true).get('length');
    }.property('@each.isCompleted'),

    hasCompleted: function () {
        return this.get('completed') > 0;
    }.property('completed'),

    /*
     * The Ember.Checkbox view passes the key, which is the name of the
     * calling method, `allAreDone`. And the value is the boolean value
     * of the checkbox.
     */
    allAreDone: function (key, value) {
        if (value !== undefined) {
            this.setEach('isCompleted', value);
            return value;
        } else {
            return !!this.get('length') &&
                this.everyProperty('isCompleted', true);
        }
    }.property('@each.isCompleted')
});

And last is the controller for the todo view. No surprises here.

Todos.TodoController = Ember.ObjectController.extend({
    isEditing: false,

    editTodo: function () {
        this.set('isEditing', true);
    },

    removeTodo: function () {
        var todo = this.get('model');

        todo.deleteRecord();
        todo.get('store').commit();
    }
});

The template

<script type="text/x-handlebars" data-template-name="todos">
    <section id="todoapp">
        <header id="header">
            <h1>todos</h1>
            {{view Ember.TextField id="new-todo" placeholder="What needs to be done?"
                         valueBinding="newTitle" action="createTodo"}}
        </header>
        {{#if length}}
            <section id="main">
                <ul id="todo-list">
                    {{#each filteredTodos itemController="todo"}}
                        <li {{bindAttr class="isCompleted:completed isEditing:editing"}}>
                            {{#if isEditing}}
                                {{view Todos.EditTodoView todoBinding="this"}}
                            {{else}}
                                {{view Ember.Checkbox checkedBinding="isCompleted" class="toggle"}}
                                <label {{action "editTodo" on="doubleClick"}}>{{title}}</label>
                                <button {{action "removeTodo"}} class="destroy"></button>
                            {{/if}}
                            </li>
                    {{/each}}
                </ul>
                {{view Ember.Checkbox id="toggle-all" checkedBinding="allAreDone"}}
            </section>
            <footer id="footer">
                <span id="todo-count">{{{remainingFormatted}}}</span>
                <ul id="filters">
                    <li>
                        {{#linkTo todos.index activeClass="selected"}}All{{/linkTo}}
                    </li>
                    <li>
                        {{#linkTo todos.active activeClass="selected"}}Active{{/linkTo}}
                    </li>
                    <li>
                        {{#linkTo todos.completed activeClass="selected"}}Completed{{/linkTo}}
                    </li>
                </ul>
                {{#if hasCompleted}}
                    <button id="clear-completed" {{action "clearCompleted"}} {{bindAttr class="buttonClass:hidden"}}>
                        Clear completed ({{completed}})
                    </button>
                {{/if}}
            </footer>
        {{/if}}
    </section>
    <footer id="info">
        <p>Double-click to edit a todo</p>
        <p>
            Created by
            <a href="http://github.com/tomdale">Tom Dale</a>,
            <a href="http://github.com/addyosmani">Addy Osmani</a>
        </p>
        <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
    </footer>
</script>

Conclusion

We now have a complete EmberJS app which interacts with our Flask-based backend. We can easily add, modify and delete todos. Don’t forget that the code for this tutorial is available on Github. As always, I’m sure there are errors. Please leave a comment or create an issue on Github.

How one shouldn't name a blog while listening to Cypress Hill

view archive