Evan You 12 лет назад
Родитель
Сommit
9a4e5d0350
6 измененных файлов с 144 добавлено и 84 удалено
  1. 1 2
      TODO.md
  2. 4 1
      component.json
  3. 16 10
      examples/todos/app.js
  4. 10 2
      examples/todos/index.html
  5. 18 48
      src/directives/each.js
  6. 95 21
      src/seed.js

+ 1 - 2
TODO.md

@@ -1,9 +1,8 @@
+- use descriptor for computed properties
 - getter setter should emit events
 - auto dependency extraction for computed properties (evaluate, record triggered getters)
-- use descriptor for computed properties
 
 - parse textNodes?
-- limited set of expressions (e.g. ternary operator)
 - more directives / filters
     - sd-if
     - sd-route

+ 4 - 1
component.json

@@ -12,5 +12,8 @@
     "src/directives/index.js",
     "src/directives/each.js",
     "src/directives/on.js"
-  ]
+  ],
+  "dependencies": {
+    "component/emitter": "*"
+  }
 }

+ 16 - 10
examples/todos/app.js

@@ -2,8 +2,8 @@ var Seed = require('seed')
 
 var todos = [
     { text: 'make nesting controllers work', done: true },
-    { text: 'complete ArrayWatcher', done: false },
-    { text: 'computed properties', done: false },
+    { text: 'complete ArrayWatcher', done: true },
+    { text: 'computed properties', done: true },
     { text: 'parse textnodes', done: false }
 ]
 
@@ -11,23 +11,29 @@ Seed.controller('Todos', function (scope) {
 
     // regular properties -----------------------------------------------------
     scope.todos = todos
-    scope.filter = window.location.hash.slice(2)
-    scope.allDone = false
+    scope.filter = window.location.hash.slice(2) || 'all'
     scope.remaining = todos.reduce(function (count, todo) {
         return count + (todo.done ? 0 : 1)
     }, 0)
+    scope.allDone = scope.remaining === 0
 
     // computed properties ----------------------------------------------------
-    scope.total = function () {
-        return scope.todos.length
+    scope.total = {
+        get: function () {
+            return scope.todos.length
+        }
     }
 
-    scope.completed = function () {
-        return scope.total() - scope.remaining
+    scope.completed = {
+        get: function () {
+            return scope.total() - scope.remaining
+        }
     }
 
-    scope.itemLabel = function () {
-        return scope.remaining > 1 ? 'items' : 'item'
+    scope.itemLabel = {
+        get: function () {
+            return scope.remaining > 1 ? 'items' : 'item'
+        }
     }
 
     // event handlers ---------------------------------------------------------

+ 10 - 2
examples/todos/index.html

@@ -21,10 +21,18 @@
             </header>
             
             <section id="main" sd-show="total < todos">
-                <input id="toggle-all" type="checkbox" sd-checked="allDone" sd-on="change:toggleAll">
+                <input 
+                    id="toggle-all"
+                    type="checkbox"
+                    sd-checked="allDone"
+                    sd-on="change:toggleAll"
+                >
                 <ul id="todo-list">
                     <!-- a single todo item -->
-                    <li sd-each="todo:todos" sd-class="completed:todo.done, editing:todo.editing">
+                    <li
+                        sd-each="todo:todos"
+                        sd-class="completed:todo.done, editing:todo.editing"
+                    >
                         <div class="view">
                             <input
                                 class="toggle"

+ 18 - 48
src/directives/each.js

@@ -1,18 +1,7 @@
 var config = require('../config')
 
-var augmentations = {
-    remove: function (scope) {
-        this.splice(scope.$index, 1)
-    },
-    replace: function (index, data) {
-        if (typeof index !== 'number') {
-            index = index.$index
-        }
-        this.splice(index, 1, data)
-    }
-}
-
 var mutationHandlers = {
+
     push: function (m) {
         var self = this
         m.args.forEach(function (data, i) {
@@ -20,9 +9,11 @@ var mutationHandlers = {
             self.container.insertBefore(seed.el, self.marker)
         })
     },
+
     pop: function (m) {
         m.result.$destroy()
     },
+
     unshift: function (m) {
         var self = this
         m.args.forEach(function (data, i) {
@@ -32,13 +23,15 @@ var mutationHandlers = {
                      : self.marker
             self.container.insertBefore(seed.el, ref)
         })
-        self.reorder()
+        self.updateIndexes()
     },
+
     shift: function (m) {
         m.result.$destroy()
         var self = this
-        self.reorder()
+        self.updateIndexes()
     },
+
     splice: function (m) {
         var self    = this,
             index   = m.args[0],
@@ -49,18 +42,19 @@ var mutationHandlers = {
         })
         if (added > 0) {
             m.args.slice(2).forEach(function (data, i) {
-                var seed  = self.buildItem(data, index + i),
-                    pos   = index - removed + added + 1,
-                    ref   = self.collection[pos]
-                          ? self.collection[pos].$seed.el
-                          : self.marker
+                var seed = self.buildItem(data, index + i),
+                    pos  = index - removed + added + 1,
+                    ref  = self.collection[pos]
+                         ? self.collection[pos].$seed.el
+                         : self.marker
                 self.container.insertBefore(seed.el, ref)
             })
         }
         if (removed !== added) {
-            self.reorder()
+            self.updateIndexes()
         }
     },
+
     sort: function () {
         var self = this
         self.collection.forEach(function (scope, i) {
@@ -69,30 +63,11 @@ var mutationHandlers = {
         })
     }
 }
-mutationHandlers.reverse = mutationHandlers.sort
-
-function watchArray (collection, callback) {
-
-    Object.keys(mutationHandlers).forEach(function (method) {
-        collection[method] = function () {
-            var result = Array.prototype[method].apply(this, arguments)
-            callback({
-                method: method,
-                args: Array.prototype.slice.call(arguments),
-                result: result
-            })
-        }
-    })
 
-    for (var method in augmentations) {
-        collection[method] = augmentations[method]
-    }
-}
+mutationHandlers.reverse = mutationHandlers.sort
 
 module.exports = {
 
-    mutationHandlers: mutationHandlers,
-
     bind: function () {
         this.el.removeAttribute(config.prefix + '-each')
         var ctn = this.container = this.el.parentNode
@@ -106,13 +81,8 @@ module.exports = {
         if (!Array.isArray(collection)) return
         this.collection = collection
         var self = this
-        watchArray(collection, function (mutation) {
-            if (self.mutationHandlers) {
-                self.mutationHandlers[mutation.method].call(self, mutation)
-            }
-            if (self.binding.refreshDependents) {
-                self.binding.refreshDependents()
-            }
+        collection.on('mutate', function (mutation) {
+            mutationHandlers[mutation.method].call(self, mutation)
         })
         collection.forEach(function (data, i) {
             var seed = self.buildItem(data, i)
@@ -134,7 +104,7 @@ module.exports = {
         return spore
     },
 
-    reorder: function () {
+    updateIndexes: function () {
         this.collection.forEach(function (scope, i) {
             scope.$index = i
         })

+ 95 - 21
src/seed.js

@@ -1,4 +1,5 @@
 var config          = require('./config'),
+    Emitter         = require('emitter'),
     DirectiveParser = require('./directive-parser'),
     TextNodeParser  = require('./textnode-parser')
 
@@ -42,16 +43,19 @@ function Seed (el, options) {
     scope.$index    = options.index
     scope.$parent   = options.parentSeed && options.parentSeed.scope
 
-    // revursively process nodes for directives
+    // update bindings when a property is set
+    this.on('set', this._updateBinding.bind(this))
+
+    // revursively compile nodes for directives
     this._compileNode(el, true)
 
     // if has controller, apply it
     var ctrlID = el.getAttribute(ctrlAttr)
     if (ctrlID) {
         el.removeAttribute(ctrlAttr)
-        var controller = config.controllers[ctrlID]
-        if (controller) {
-            controller.call(this, this.scope)
+        var factory = config.controllers[ctrlID]
+        if (factory) {
+            factory.call(this, this.scope)
         } else {
             console.warn('controller ' + ctrlID + ' is not defined.')
         }
@@ -59,11 +63,11 @@ function Seed (el, options) {
 }
 
 Seed.prototype._compileNode = function (node, root) {
-    var self = this
+    var seed = this
 
     if (node.nodeType === 3) { // text node
 
-        self._compileTextNode(node)
+        seed._compileTextNode(node)
 
     } else if (node.nodeType === 1) {
 
@@ -74,7 +78,7 @@ Seed.prototype._compileNode = function (node, root) {
 
             var binding = DirectiveParser.parse(eachAttr, eachExp)
             if (binding) {
-                self._bind(node, binding)
+                seed._bind(node, binding)
             }
 
         } else if (ctrlExp && !root) { // nested controllers
@@ -82,10 +86,10 @@ Seed.prototype._compileNode = function (node, root) {
             var id = node.id,
                 seed = new Seed(node, {
                     child: true,
-                    parentSeed: self
+                    parentSeed: seed
                 })
             if (id) {
-                self['$' + id] = seed
+                seed['$' + id] = seed
             }
 
         } else { // normal node
@@ -96,10 +100,10 @@ Seed.prototype._compileNode = function (node, root) {
                     if (attr.name === ctrlAttr) return
                     var valid = false
                     attr.value.split(',').forEach(function (exp) {
-                        var binding = DirectiveParser.parse(attr.name, exp)
-                        if (binding) {
+                        var directive = DirectiveParser.parse(attr.name, exp)
+                        if (directive) {
                             valid = true
-                            self._bind(node, binding)
+                            seed._bind(node, directive)
                         }
                     })
                     if (valid) node.removeAttribute(attr.name)
@@ -109,7 +113,7 @@ Seed.prototype._compileNode = function (node, root) {
             // recursively compile childNodes
             if (node.childNodes.length) {
                 slice.call(node.childNodes).forEach(function (child) {
-                    self._compileNode(child)
+                    seed._compileNode(child)
                 })
             }
         }
@@ -187,26 +191,56 @@ Seed.prototype._createBinding = function (key) {
     this._bindings[key] = binding
 
     // bind accessor triggers to scope
+    var seed = this
     Object.defineProperty(this.scope, key, {
         get: function () {
+            seed.emit('get', key)
             return binding.value
         },
         set: function (value) {
             if (value === binding.value) return
-            binding.changed = true
-            binding.value = value
-            binding.instances.forEach(function (instance) {
-                instance.update(value)
-            })
-            if (binding.refreshDependents) {
-                binding.refreshDependents()
-            }
+            seed.emit('set', key, value)
         }
     })
 
     return binding
 }
 
+Seed.prototype._updateBinding = function (key, value) {
+
+    var binding = this._bindings[key],
+        type = typeOf(value)
+
+    if (type === 'Object') {
+        if (value.get) { // computed property
+            type = 'Computed'
+            value = value.get
+        }
+    } else if (type === 'Array') {
+        augmentArray(value)
+        value.on('mutate', function () {
+            if (binding.dependents) {
+                binding.refreshDependents()
+            }
+        })
+    }
+
+    binding.type = type
+    binding.value = value
+    binding.changed = true
+
+    // update all instances
+    binding.instances.forEach(function (instance) {
+        instance.update(value)
+    })
+
+    // notify dependents to refresh themselves
+    if (binding.dependents) {
+        binding.refreshDependents()
+    }
+
+}
+
 Seed.prototype._unbind = function () {
     var unbind = function (instance) {
         if (instance.unbind) {
@@ -248,6 +282,8 @@ Seed.prototype._dump = function () {
 
 // Helpers --------------------------------------------------------------------
 
+// determine which scope a key belongs to
+// based on nesting symbols
 function determinScope (key, scope) {
     if (key.nesting) {
         var levels = key.nesting
@@ -262,4 +298,42 @@ function determinScope (key, scope) {
     return scope
 }
 
+// get accurate type of an object
+var OtoString = Object.prototype.toString
+function typeOf (obj) {
+    return OtoString.call(obj).slice(8, -1)
+}
+
+// augment an Array so that it emit events when mutated
+var arrayMutators = ['push','pop','shift','unshift','splice','sort','reverse']
+var arrayAugmentations = {
+    remove: function (scope) {
+        this.splice(scope.$index, 1)
+    },
+    replace: function (index, data) {
+        if (typeof index !== 'number') {
+            index = index.$index
+        }
+        this.splice(index, 1, data)
+    }
+}
+function augmentArray (collection) {
+    Emitter(collection)
+    arrayMutators.forEach(function (method) {
+        collection[method] = function () {
+            var result = Array.prototype[method].apply(this, arguments)
+            collection.emit('mutate', {
+                method: method,
+                args: Array.prototype.slice.call(arguments),
+                result: result
+            })
+        }
+    })
+    for (var method in arrayAugmentations) {
+        collection[method] = arrayAugmentations[method]
+    }
+}
+
+Emitter(Seed.prototype)
+
 module.exports = Seed