PageRenderTime 85ms CodeModel.GetById 16ms RepoModel.GetById 7ms app.codeStats 0ms

/_posts/2013-01-24-backbone-tutorial-9.md

http://github.com/alexyoung/dailyjs
Markdown | 273 lines | 204 code | 69 blank | 0 comment | 0 complexity | 4285c1450e15113f4f3c1fa09f01445c MD5 | raw file
Possible License(s): CC-BY-SA-3.0
  1. ---
  2. layout: post
  3. title: "Backbone.js Tutorial: Tasks"
  4. author: Alex Young
  5. categories:
  6. - backbone.js
  7. - mvc
  8. - node
  9. - backgoog
  10. - fastfood
  11. ---
  12. <ul class="parts">
  13. <li><a href="http://dailyjs.com/2012/11/29/backbone-tutorial-1/">Part 1: Build Environment</a></li>
  14. <li><a href="http://dailyjs.com/2012/12/06/backbone-tutorial-2/">Part 2: Google's APIs and RequireJS</a></li>
  15. <li><a href="http://dailyjs.com/2012/12/13/backbone-tutorial-3/">Part 3: Authenticating with OAuth2</a></li>
  16. <li><a href="http://dailyjs.com/2012/12/20/backbone-tutorial-4/">Part 4: Backbone.sync</a></li>
  17. <li><a href="http://dailyjs.com/2012/12/27/backbone-tutorial-5/">Part 5: List Views</a></li>
  18. <li><a href="http://dailyjs.com/2013/01/03/backbone-tutorial-6/">Part 6: Creating Lists</a></li>
  19. <li><a href="http://dailyjs.com/2013/01/10/backbone-tutorial-7/">Part 7: Editing Lists</a></li>
  20. <li><a href="http://dailyjs.com/2013/01/17/backbone-tutorial-8/">Part 8: Deleting Lists</a></li>
  21. <li><a href="http://dailyjs.com/2013/01/24/backbone-tutorial-9/"><strong>Part 9: Tasks</strong></a></li>
  22. <li><a href="http://dailyjs.com/2013/01/31/backbone-tutorial-10/">Part 10: Oh No Not More Tasks</a></li>
  23. <li><a href="http://dailyjs.com/2013/02/07/backbone-tutorial-11/">Part 11: Spies, Stubs, and Mocks</a></li>
  24. <li><a href="http://dailyjs.com/2013/02/14/backbone-tutorial-12/">Part 12: Testing with Mocks</a></li>
  25. <li><a href="http://dailyjs.com/2013/03/07/backbone-tutorial-13/">Part 13: Routes</a></li>
  26. <li><a href="http://dailyjs.com/2013/03/14/backbone-tutorial-14/">Part 14: Customosing the UI</a></li>
  27. <li><a href="http://dailyjs.com/2013/03/28/backbone-tutorial-15/">Part 15: Updates for 1.0, Clear Complete</a></li>
  28. <li><a href="http://dailyjs.com/2013/04/04/backbone-tutorial-16/">Part 16: jQuery Plugins</a></li>
  29. </ul>
  30. ###Preparation
  31. Before starting this tutorial, you'll need the following:
  32. * [alexyoung / dailyjs-backbone-tutorial](https://github.com/alexyoung/dailyjs-backbone-tutorial) at commit `8d88095`
  33. * The API key from part 2
  34. * The "Client ID" key from part 2
  35. * Update `app/js/config.js` with your keys (if you've checked out my source)
  36. To check out the source, run the following commands (or use a suitable Git GUI tool):
  37. {% highlight text %}
  38. git clone git@github.com:alexyoung/dailyjs-backbone-tutorial.git
  39. cd dailyjs-backbone-tutorial
  40. git reset --hard 8d88095
  41. {% endhighlight %}
  42. ###Tasks CRUD
  43. Things have been quiet in the Tasks Bootstrap project over the last few weeks. Lists have appeared, but there's been nary a Hamburglar or Burger King in sight. How do we attract those all important fast food mascots to our project? By adding support for tasks of course! How else can they write their extensive lists of upcoming franchise inspections and special edition McRibs?
  44. This tutorial will cover the following:
  45. * Creating a view for a single task
  46. * Creating a view for a list of tasks
  47. * Adding the tasks collection
  48. * Fetching tasks from Google's API
  49. The really interesting part that you'll want to remember is dealing with the relationship between a parent view and child views. Backbone doesn't specifically address relationships between models, or views. In this example, we ideally want to say _tasks belong to lists_ or _task views belong to list views_. However, there isn't a de facto way of expressing such relationships. There are libraries out there to do it, but I'll show you how to think about things in pure Backbone/Underscore.
  50. ###Boilerplate
  51. Before you get started, create some new directories:
  52. {% highlight text %}
  53. $ mkdir app/js/views/tasks
  54. $ mkdir app/js/templates/tasks
  55. {% endhighlight %}
  56. And add a new collection to `app/js/collections/tasks.js`:
  57. {% highlight javascript %}
  58. define(['models/task'], function(Task) {
  59. var Tasks = Backbone.Collection.extend({
  60. model: Task,
  61. url: 'tasks'
  62. });
  63. return Tasks;
  64. });
  65. {% endhighlight %}
  66. The `Tasks` collection doesn't do anything you haven't seen before. Fetching tasks with Google's API requires a `tasklist`, so you have to call `fetch` with an additional parameter:
  67. {% highlight javascript %}
  68. collection.fetch({ data: { tasklist: this.model.get('id') }, // ...
  69. {% endhighlight %}
  70. It's cool though, because we handled fetching `TaskLists` like that when we passed `{ userId: '@me' }` so it feels consistent within the context of this project.
  71. The template that contains the tasks view includes a form for creating new tasks, a container for the task list, and another container for the currently selected task (so it can be edited). This file should be saved as `app/js/templates/index.js`:
  72. {% highlight html %}
  73. <div class="span6">
  74. <div id="add-task">
  75. <form class="well row form-inline add-task">
  76. <input type="text" class="pull-left" placeholder="Enter a new task's title and press return" name="title">
  77. <button type="submit" class="pull-right btn"><i class="icon-plus"></i></button>
  78. </form>
  79. </div>
  80. <ul id="task-list"></ul>
  81. </div>
  82. <div class="span6">
  83. <div id="selected-task"></div>
  84. <div class="alert" id="warning-no-task-selected">
  85. <strong>Note:</strong> Select a task to edit or delete it.
  86. </div>
  87. </div>
  88. {% endhighlight %}
  89. This uses some [Bootstrap](http://twitter.github.com/bootstrap/) classes for creating columns. The `TaskView`, in `app/js/templates/tasks/task.html` has a few elements to contain the title, notes, and a checkbox for toggling the task's state:
  90. {% highlight javascript %}
  91. <input type="checkbox" data-task-id="{{id}}" name="task_check_{{id}}" class="check-task" value="t">
  92. <span class="title {{status}}">{{title}}</span>
  93. <span class="notes">{{notes}}</span>
  94. {% endhighlight %}
  95. ###Views
  96. The main `TasksIndexView` loads the tasks using the `Tasks` collection, and then renders them using `TaskView`. This is the source for `TasksIndexView` in `app/js/views/tasks/index.js`:
  97. {% highlight javascript %}
  98. define(['text!templates/tasks/index.html', 'views/tasks/task', 'collections/tasks'], function(template, TaskView, Tasks) {
  99. var TasksIndexView = Backbone.View.extend({
  100. tagName: 'div',
  101. className: 'row-fluid',
  102. template: _.template(template),
  103. events: {
  104. 'submit .add-task': 'addTask'
  105. },
  106. initialize: function() {
  107. this.children = [];
  108. },
  109. addTask: function() {
  110. },
  111. render: function() {
  112. this.$el.html(this.template());
  113. var $el = this.$el.find('#task-list')
  114. , self = this;
  115. this.collection = new Tasks();
  116. this.collection.fetch({ data: { tasklist: this.model.get('id') }, success: function() {
  117. self.collection.each(function(task) {
  118. var item = new TaskView({ model: task, parentView: self });
  119. $el.append(item.render().el);
  120. self.children.push(item);
  121. });
  122. }});
  123. return this;
  124. }
  125. });
  126. return TasksIndexView;
  127. });
  128. {% endhighlight %}
  129. This loads the tasks using `collection.fetch`, and then appends a `TaskView` for each task. This is `TaskView`:
  130. {% highlight javascript %}
  131. define(['text!templates/tasks/task.html'], function(template) {
  132. var TaskView = Backbone.View.extend({
  133. tagName: 'li',
  134. className: 'controls well task row',
  135. template: _.template(template),
  136. events: {
  137. 'click': 'open'
  138. },
  139. initialize: function(options) {
  140. this.parentView = options.parentView;
  141. },
  142. render: function(e) {
  143. var $el = $(this.el);
  144. $el.data('taskId', this.model.get('id'));
  145. $el.html(this.template(this.model.toJSON()));
  146. $el.find('.check-task').attr('checked', this.model.get('status') === 'completed');
  147. return this;
  148. },
  149. open: function(e) {
  150. if (this.parentView.activeTaskView) {
  151. this.parentView.activeTaskView.close();
  152. }
  153. this.$el.addClass('active');
  154. this.parentView.activeTaskView = this;
  155. },
  156. close: function(e) {
  157. this.$el.removeClass('active');
  158. }
  159. });
  160. return TaskView;
  161. });
  162. {% endhighlight %}
  163. The parent view is tracked so `open` can determine if another task has been clicked on, and if so "deactivate" it (remove the `active` class). There are many ways to do this: I've seen people iterating over views to close all of them, using `$('selector').removeClass('active')` to remove all related items with an `active` class, or triggering events on models. I feel like view-related code should be handled in views, and models and collections should do their own specific jobs.
  164. Next you'll need to add `TasksIndexView` to the `define` in `app/js/views/lists/menuitem.js` and change the `open` method to instantiate a `TasksIndexView`:
  165. {% highlight javascript %}
  166. open: function() {
  167. if (bTask.views.activeListMenuItem) {
  168. bTask.views.activeListMenuItem.$el.removeClass('active');
  169. }
  170. bTask.views.activeListMenuItem = this;
  171. this.$el.addClass('active');
  172. // Render the tasks
  173. if (bTask.views.tasksIndexView) {
  174. bTask.views.tasksIndexView.remove();
  175. }
  176. bTask.views.tasksIndexView = new TasksIndexView({ collection: bTask.collections.tasks, model: this.model });
  177. bTask.views.app.$el.find('#tasks-container').html(bTask.views.tasksIndexView.render().el);
  178. return false;
  179. }
  180. {% endhighlight %}
  181. It tracks the last instance of `TasksIndexView` so it can remove it manually. It's usually a good idea to call `remove` so events can be unbound before views go out of scope -- I'll write a tutorial about Backbone and garbage collection later on.
  182. I also added some defaults to the `Task` model (in `app/js/models/task.js`):
  183. {% highlight javascript %}
  184. define(function() {
  185. var Task = Backbone.Model.extend({
  186. url: 'tasks',
  187. defaults: { title: '', notes: '' }
  188. });
  189. return Task;
  190. });
  191. {% endhighlight %}
  192. The reason I did this was the `TaskView` will raise errors when interpolating using a model that doesn't have a title or notes -- it's quite common for tasks in Google Tasks to not have any notes.
  193. With these templates, views, and changes, you should be able to select lists and see their tasks, and also select tasks.
  194. ###Styles
  195. ![Bootstrap styles](/images/posts/backbone-9-styles.png)
  196. As it stands, the application doesn't make a lot of visual sense. I've added Bootstrap -- this just required downloading the CSS and image files and putting them in `app/css` and `app/img`. Also, `app/index.html` loads `css/bootstrap.min.css`.
  197. I added some custom styles to create a panel-based layout that shows the tasks in a similar way to [Things](http://culturedcode.com/things/).
  198. ###Backbone 0.9.10
  199. I've updated Backbone to 0.9.10 and added it to the repository. I had to change the `Backbone.sync` method to use a different signature when calling `options.success`, in `app/js/gapi.js`:
  200. {% highlight javascript %}
  201. options.success(model, result, request);
  202. {% endhighlight %}
  203. ###Summary
  204. The full source for this tutorial can be found in [alexyoung / dailyjs-backbone-tutorial, commit 0491ad](https://github.com/alexyoung/dailyjs-backbone-tutorial/commit/0491ad6e7de28ccfe0cab59138a93c469a3f2a7e).