2012-02-28

Backbone.js 與 CoffeeScript

參考了一些 Backbone 搭配 CoffeeScript 的文章:


之後, 將 Backbone 範例 Todos 改用 CoffeeScript 重寫練習, 果然一開始就踩到地雷。
改寫之後, 是完全不會動的, 後來發現 initialize 方法根本沒被觸發, 但 constructor 有被觸發, 把 constructor 註解後就可以了。也就是使用 initialize 方法, 而不用 constructor。

另外, Backbone 原本的繼承寫法是呼叫 extend 方法, 除了改用 extends 繼承寫法之外, 其初設物件 (第一個參數), 可以放到 initialize 方法裡做初始化的動作。

2012-04-02 補個程式碼
bb_todos.html ----------------------------
<!DOCTYPE html>
<html>
  <head>
    <title>Backbone Demo: Todos</title>
    <link href="todos.css" media="all" rel="stylesheet" type="text/css"/>
    <script src="http://jashkenas.github.com/coffee-script/extras/coffee-script.js"
        type="text/javascript" charset="utf-8"></script>
    <script src="http://documentcloud.github.com/backbone/test/vendor/json2.js"></script>
    <script src="http://documentcloud.github.com/backbone/test/vendor/jquery-1.7.1.js"></script>
    <script src="http://documentcloud.github.com/backbone/test/vendor/underscore-1.3.1.js"></script>
    <script src="http://documentcloud.github.com/backbone/backbone.js"></script>
    <script type="text/coffeescript" src="backbone-localstorage.coffee"></script>
    <script type="text/coffeescript" src="todos.coffee"></script>
  </head>

  <body>

    <!-- Todo App Interface -->
    <div id="todoapp">
      <div class="title">
        <h1>Todos</h1>
      </div>
      <div class="content">
        <div id="create-todo">
          <input id="new-todo" placeholder="What needs to be done?" type="text" />
          <span class="ui-tooltip-top" style="display:none;">Press Enter to save this task</span>
        </div>

        <div id="todos">
          <ul id="todo-list"></ul>
        </div>

        <div id="todo-stats"></div>

      </div>
    </div>

    <ul id="instructions">
      <li>Double-click to edit a todo.</li>
      <li><a href="../../docs/todos.html">View the annotated source.</a></li>
    </ul>

    <div id="credits">
      Created by
      <br />
      <a href="http://jgn.me/">J&eacute;r&ocirc;me Gravel-Niquet</a>
    </div>

    <!-- Templates -->
    <script type="text/template" id="item-template">
      <div class="todo <%= done ? 'done' : '' %>">
        <div class="display">
          <input class="check" type="checkbox" <%= done ? 'checked="checked"' : '' %> />
          <div class="todo-text"></div>
          <span class="todo-destroy"></span>
        </div>
        <div class="edit">
          <input class="todo-input" type="text" value="" />
        </div>
      </div>
    </script>

    <script type="text/template" id="stats-template">
      <% if (total) { %>
        <span class="todo-count">
          <span class="number"><%= remaining %></span>
          <span class="word"><%= remaining == 1 ? 'item' : 'items' %></span> left.
        </span>
      <% } %>
      <% if (done) { %>
        <span class="todo-clear">
          <a href="#">
            Clear <span class="number-done"><%= done %></span>
            completed <span class="word-done"><%= done == 1 ? 'item' : 'items' %></span>
          </a>
        </span>
      <% } %>
    </script>

  </body>
</html>
backbone-localstorage.coffee ----------------------------
window.s4= -> 
  (((1+Math.random())*0x10000)|0).toString(16).substring(1)

window.guid= ->
   "#{s4()}#{s4()}-#{s4()}-#{s4()}-#{s4()}-#{s4()}#{s4()}#{s4()}"

class window.Store
  constructor: (@name) ->
    store = localStorage.getItem @name
    @data = (store && JSON.parse(store)) || {};

  save: -> localStorage.setItem @name, JSON.stringify(@data)

  create: (model)->
    if ! model.id
      model.id = model.attributes.id = guid()
    @data[model.id] = model
    @save()
    model

  update: (model)->
    @data[model.id] = model
    @save()
    model

  find: (model)->
    @data[model.id]

  findAll: ->
    _.values @data

  destroy: (model)->
    delete @data[model.id]
    @save()
    model

Backbone.sync = (method, model, options)->
  store = model.localStorage || model.collection.localStorage
  switch method
    when 'read'
      resp = if model.id then store.find(model) else store.findAll()
    when 'create'
      resp = store.create model
    when 'update' 
      resp = store.update model
    when 'delete'
      resp = store.destroy model
  if resp
    options.success resp
  else
    options.error "Record not found"
todos.coffee ----------------------------
$ = jQuery
$ ->
  class window.Todo extends Backbone.Model
    defaults: ->
      done: false
      order: Todos.nextOrder()
    toggle: ->
      @save done: ! @get("done")

  class window.TodoList extends Backbone.Collection
    initialize: ->
      @model = Todo
      @localStorage = new Store "todos"

    done: ->
      @filter (todo)-> todo.get 'done'

    remaining: -> @without.apply this, @done()

    nextOrder: ->
      return 1 if ! @length
      @last().get('order') + 1

    comparator: (todo)->
      todo.get 'order'

  class window.TodoView extends Backbone.View
    tagName : "li"
    events :
      "click .check"              : "toggleDone"
      "dblclick div.todo-text"    : "edit"
      "click span.todo-destroy"   : "clear"
      "keypress .todo-input"      : "updateOnEnter"
    template : _.template $('#item-template').html()
    initialize: ->
      @model.bind 'change', @render, this
      @model.bind 'destroy', @remove, this

    render: ->
      $(@el).html @template @model.toJSON()
      @setText()
      @

    setText: ->
      text = @model.get 'text'
      @$('.todo-text').text text
      @input = @$('.todo-input')
      @input.bind('blur', _.bind(@close, this)).val text

    toggleDone: ->
      @model.toggle()

    edit: ->
      $(@el).addClass 'editing'
      @input.focus()

    close: ->
      @model.save {text: @input.val() }
      $(@el).removeClass 'editing'

    updateOnEnter: (e)->
      @close if e.keyCode is 13

    remove: ->
      $(@el).remove()

    clear: ->
      @model.destroy()

  class window.AppView extends Backbone.View
    el : $('#todoapp')
    statsTemplate : _.template $('#stats-template').html()
    events :
      "keypress #new-todo":  "createOnEnter"
      "keyup #new-todo":     "showTooltip"
      "click .todo-clear a": "clearCompleted"
    initialize: ->
      @input = @$('#new-todo')
      Todos.bind 'add',   @addOne, @
      Todos.bind 'reset', @addAll, @
      Todos.bind 'all',   @render, @
      Todos.fetch()

    render: ->
      @$('#todo-stats').html @statsTemplate
        total:      Todos.length
        done:       Todos.done().length
        remaining:  Todos.remaining().length

    addOne: (todo)->
      view = new TodoView {model: todo}
      $('#todo-list').append view.render().el

    addAll: ->
      Todos.each @addOne

    createOnEnter: (e)->
      text = @input.val()
      return if not text or e.keyCode isnt 13
      Todos.create text:text
      @input.val('')

    clearCompleted: ->
      _.each Todos.done(), (todo)->todo.destroy()
      false

    showTooltip: (e)->
      tooltip = @$('.ui-tooltip-top')
      val = @input.val()
      tooltip.fadeOut
      clearTimeout @tooltipTimeout if @tooltipTimeout
      return if val is '' or val is @input.attr('placeholder')
      show = -> tooltip.show().fadeIn()
      @tooltipTimeout = _.delay show, 1000

  window.Todos = new TodoList
  window.App = new AppView

3 則留言:

Shinder 提到...

1. constructor 還是可以用, 不過要手動呼叫父類別的建構子。
2. 感覺初始化物件還是放在屬性比較好。
3. 所有的定義請放在 $ -> 底下。
4. 改寫完了還是有些 bugs...

Shinder 提到...

改好了, 補回誤刪的 @ 就好了。

Shinder 提到...

改寫成 CoffeeScript 後的內容:
https://docs.google.com/document/d/12Kgs_jJ4hpeHmMjtWg_IxGxa3CFjx7Coyqs7k4BKvK0/edit

FB 留言