准备

开始本教程之前,你需要了解以下几点:

  • alexyoung / dailyjs-backbone-tutorial提交到了0491ad版本
  • 第二部分中的API key
  • 第二部分中的“Client ID”
  • 更新app/js/config.js成你自己的key(如果你是检出的代码)

要检出源码,请运行以下命令(或用Git GUI工具):

git clone git@github.com:alexyoung/dailyjs-backbone-tutorial.git
cd dailyjs-backbone-tutorial
git reset --hard 0491ad

任务项

enter image description here

如图所示任务项的相关功能界面

可以看到我们还有很多功能要实现,现在我们只是实现了任务项列表,还没有实现交互,下面将分几个小节来实现:

  • 添加任务项
  • 编辑任务项
  • 删除任务项
  • 切换任务项状态

大部分的内容其实我们在实现任务列表的时候都已经讲到过了,这部分只是带大家巩固下Backbone一下用法。

添加任务项

app/js/views/tasks/index.js中,确定我们有一个addTask事件:

events: {
  'submit .add-task': 'addTask'
},

initialize方法中,添加监听到这个类的集合

initialize: function() {
  this.children = [];
  this.collection.on('add', this.renderTask, this);
},

这样就实现了当我们集合中有新任务项被添加的时候会自动渲染任务项列表。

addTask函数里会使用Task.prototype.save中的Google’s API来实现任务项保存到服务端,成功回调后会添加任务项到集合中,这样就实现了新任务项的显示,默认我们添加了Google’s Tasks指定的参数{ at: 0}是让新任务项显示在最前面,当然这需要连网支持,也可以先保存到本地然后同步到Google,但这个不是我们的重点。

addTask: function() {
  var $input = this.$el.find('input[name="title"]')
    , task = new this.collection.model({ tasklist: this.model.get('id') })
    , self = this
    ;

  task.save({ title: $input.val() }, {
    success: function() {
      self.collection.add(task, { at: 0 });
    }
  });
  $input.val('');

  return false;
},

renderTask: function(task, list, options) {
  var item = new TaskView({ model: task, parentView: this })
    , $el = this.$el.find('#task-list');
  if (options && options.at === 0) {
    $el.prepend(item.render().el);
  } else {
    $el.append(item.render().el);
  }
  this.children.push(item);
},

renderTask方法里有个option参数主要是用来确定如何将任务项添加到列表中,如果我们不想让新添加的任务项到最前面,我们就可以重构下render如下:

render: function() {
  this.$el.html(this.template());

  var $el = this.$el.find('#task-list')
    , self = this;

  this.collection.fetch({ data: { tasklist: this.model.get('id') }, success: function() {
    self.collection.each(function(task) {
      task.set('tasklist', self.model.get('id'));
      self.renderTask(task);
    });
  }});
}

打开app/js/views/lists/menuitem.js,修改open方法,通过Tasks集合来实例化bTask.views.tasksIndexView视图:

bTask.views.tasksIndexView = new TasksIndexView({ 
       collection: new Tasks({ tasklist: this.model.get('id') }), 
       model: this.model 
 });

然后在define中加入Tasks: define(['text!templates/lists/menuitem.html', 'views/tasks/index', 'collections/tasks'], function(template, TasksIndexView, Tasks) {

要使得Google’s API可以起作用,我们还需要修改下app/js/gapi.js,给requestContent添加tasklistID:

Backbone.sync = function(method, model, options) {
  var requestContent = {};
  options || (options = {});

  switch (model.url) {
    case 'tasks':
      requestContent.task = model.get('id');
      requestContent.tasklist = model.get('tasklist');
    break;

添加任务项到这里就已经OK了,这里我们没有新建模板是因为我们在其他部分已经写过了。

编辑任务项

要实现编辑任务项,我们需要:

  • 一个表单模板
  • 一个Backbone.View
  • 保存时的触发事件

新建一个表单模板app/js/templates/tasks/edit.html

<fieldset>
  <legend>
    Task Properties
    <a href="#" data-task-id="" class="pull-right delete-task btn"><i class="icon-trash"></i></a>
  </legend>
  <div class="control-group">
    <label for="task_title">Title</label>
    <input type="text" class="input-block-level" name="title" id="task_title" value="" placeholder="The task's title">
  </div>
  <div class="control-group">
    <label class="radio"><input type="radio" name="status" value="needsAction" > Needs action</label>
    <label class="radio"><input type="radio" name="status" value="completed" > Complete</label>
  </div>
  </div>
  <div class="control-group">
    <label for="task_notes">Notes</label>
    <textarea class="input-block-level" name="notes" id="task_notes" placeholder="Notes about this task"></textarea>
  </div>
</fieldset>
<div class="form-actions">
  <button type="submit" class="btn btn-primary">Save Changes</button>
  <button class="cancel btn">Close</button>
</div>

模板中我们使用了Bootstrap一些标签和样式,这样能好看些。

相应的视图app/js/views/tasks/edit.js如下:

define(['text!templates/tasks/edit.html'], function(template) {
  var TaskEditView = Backbone.View.extend({
    tagName: 'form',
    className: 'well edit-task',
    template: _.template(template),

    events: {
      'submit': 'submit'
    , 'click .cancel': 'cancel'
    },

    initialize: function() {
      this.model.on('change', this.render, this);
    },

    render: function() {
      this.$el.html(this.template(this.model.toJSON()));
      return this;
    },

    submit: function() {
      var title = this.$el.find('input[name="title"]').val()
        , notes = this.$el.find('textarea[name="notes"]').val()
        , status = this.$el.find('input[name="status"]:checked').val()
        ;

      this.model.set('title', title);
      this.model.set('notes', notes);

      if (status !== this.model.get('status')) {
        this.model.set('status', status);
        if (status === 'needsAction') {
          this.model.set('completed', null);
        }
      }

      this.model.save();
      return false;
    },

    cancel: function() {
      this.remove();
      return false;
    }
  });

  return TaskEditView;
});

表单提交的时候我们触发submit方法,关闭的时候我们会有相应的cancel方法。

app/js/views/tasks/index.js中添加一个方法,来实例化TaskEditView

editTask: function(task) {
  if (this.taskEditView) {
    this.taskEditView.remove();
  }
  this.taskEditView = new TaskEditView({ model: task });
  this.$el.find('#selected-task').append(this.taskEditView.render().el);
}

保证TaskEditView加载:

define(['text!templates/tasks/index.html', 'views/tasks/task', 'views/tasks/edit', 'collections/tasks'], 
function(template, TaskView, TaskEditView, Tasks) {

同时我们还要知道修改的是哪个任务项实例,所以把如下代码加app/js/views/tasks/task.jsopen方法中:

this.parentView.editTask(this.model);

写两个视图之间有很多的耦合, 这样TaskView很难再服用,虽然看起来好像TasksIndexView没有用,这里我们要怎么才能写出更易维护的Backbone代码,需要我们自己多思考下。

删除任务项

app/js/views/tasks/edit.js文件中添加destroy方法:

destroy: function() {
  this.model.destroy();
  return false;
}

然后给带垃圾桶图标(类名为.delete-task)的标签绑定上这个方法,这样当这个模型被删除就可以触发这个事件:

events: {
  'submit': 'submit'
, 'click .cancel': 'cancel'
, 'click .delete-task': 'destroy'
},

initialize: function() {
  this.model.on('change', this.render, this);
  this.model.on('destroy', this.remove, this);
},

切换任务项状态

我们有个带图标标签用来切换任务项状态,随着这一变化,应用程序将真正看起来一个真正的待办事项列表的应用程序了,打开app/js/views/tasks/task.js给列表的复选框添加一个叫change的事件:

events: {
  'click': 'open'
, 'change .check-task': 'toggle'
},

然后我们基于这个复选框的状态通过toggle方法来实现status属性的切换:

toggle: function() {
  var id = this.model.get('id')
    , $el = this.$el.find('.check-task')
    ;

  this.model.set('status', $el.attr('checked') ? 'completed' : 'needsAction');
  if (this.model.get('status') === 'needsAction') {
    this.model.set('completed', null);
  }

  this.model.save();
  return false;
}

依照Google针对任务项状态的命名规范,还有completedneedsAction,如需了解更多的可以去看看文档。

总结

到这里,我们要熟悉这些陌生的特性APIs需要很多耐心。如果你尝试运行这个项目的代码,请确保你确实在Gmail里有一些任务—如没任务将运行不起来,这我还在之后修复这个它。

教程中完整的代码可以在这里找到:alexyoung / dailyjs-backbone-tutorial, commit 0491ad