Sfoglia il codice sorgente

clean up, trying to fix delegation after array reset

Evan You 13 anni fa
parent
commit
c0a65ddda5
7 ha cambiato i file con 177 aggiunte e 129 eliminazioni
  1. 8 4
      Gruntfile.js
  2. 1 1
      examples/todos/app.js
  3. 36 21
      src/directive-parser.js
  4. 11 0
      src/directives/each.js
  5. 22 28
      src/directives/on.js
  6. 21 9
      src/main.js
  7. 78 66
      src/seed.js

+ 8 - 4
Gruntfile.js

@@ -3,14 +3,18 @@ module.exports = function( grunt ) {
     grunt.initConfig({
 
         component_build: {
-            build: {
+            dev: {
                 output: './dist/',
                 name: 'seed',
                 dev: true,
                 sourceUrls: true,
                 styles: false,
-                scripts: true,
                 verbose: true
+            },
+            build: {
+                output: './dist/',
+                name: 'seed',
+                styles: false
             }
         },
 
@@ -51,7 +55,7 @@ module.exports = function( grunt ) {
             },
             component: {
                 files: ['src/**/*.js', 'component.json'],
-                tasks: 'component_build'
+                tasks: 'component_build:dev'
             }
         }
 
@@ -63,6 +67,6 @@ module.exports = function( grunt ) {
     grunt.loadNpmTasks( 'grunt-component-build' )
     grunt.loadNpmTasks( 'grunt-mocha' )
     grunt.registerTask( 'test', ['mocha'] )
-    grunt.registerTask( 'default', ['jshint', 'component_build', 'uglify'] )
+    grunt.registerTask( 'default', ['jshint', 'component_build:build', 'uglify'] )
     
 }

+ 1 - 1
examples/todos/app.js

@@ -77,4 +77,4 @@ Seed.controller('Todos', function (scope) {
 
 })
 
-var app = Seed.bootstrap()
+Seed.bootstrap()

+ 36 - 21
src/directive-parser.js

@@ -6,11 +6,12 @@ var KEY_RE          = /^[^\|<]+/,
     ARG_RE          = /([^:]+):(.+)$/,
     FILTERS_RE      = /\|[^\|<]+/g,
     FILTER_TOKEN_RE = /[^\s']+|'[^']+'/g,
-    DEPS_RE         = /<[^<\|]+/g,
-    INVERSE_RE      = /^!/
+    INVERSE_RE      = /^!/,
     NESTING_RE      = /^\^+/
 
-// parse a key, extract argument and nesting/root info
+/*
+ *  parse a key, extract argument and nesting/root info
+ */
 function parseKey (rawKey) {
 
     var res = {},
@@ -45,6 +46,9 @@ function parseKey (rawKey) {
     return res
 }
 
+/*
+ *  parse a filter expression
+ */
 function parseFilter (filter) {
 
     var tokens = filter.slice(1)
@@ -62,6 +66,10 @@ function parseFilter (filter) {
     }
 }
 
+/*
+ *  Directive class
+ *  represents a single instance of DOM-data connection
+ */
 function Directive (directiveName, expression) {
 
     var prop, directive = directives[directiveName]
@@ -78,11 +86,10 @@ function Directive (directiveName, expression) {
     }
 
     this.directiveName = directiveName
-    this.expression = expression
-
-    var rawKey   = expression.match(KEY_RE)[0],
-        keyInfo  = parseKey(rawKey)
-
+    this.expression    = expression.trim()
+    this.rawKey        = expression.match(KEY_RE)[0]
+    
+    var keyInfo  = parseKey(this.rawKey)
     for (prop in keyInfo) {
         this[prop] = keyInfo[prop]
     }
@@ -93,22 +100,24 @@ function Directive (directiveName, expression) {
         : null
 }
 
-// called when a dependency has changed
+/*
+ *  called when a dependency has changed
+ *  computed properties only
+ */
 Directive.prototype.refresh = function () {
-    var getter = this.value
-    if (getter && typeof getter === 'function') {
-        var value = getter.call(this.seed.scope)
-        if (this.inverse) value = !value
-        this._update(
-            this.filters
-            ? this.applyFilters(value)
-            : value
-        )
-    }
+    var value = this.value.get()
+    if (this.inverse) value = !value
+    this._update(
+        this.filters
+        ? this.applyFilters(value)
+        : value
+    )
     this.binding.emitChange()
 }
 
-// called when a new value is set
+/*
+ *  called when a new value is set 
+ */
 Directive.prototype.update = function (value) {
     if (value && (value === this.value)) return
     this.value = value
@@ -127,6 +136,9 @@ Directive.prototype.update = function (value) {
     }
 }
 
+/*
+ *  pipe the value through filters
+ */
 Directive.prototype.applyFilters = function (value) {
     var filtered = value
     this.filters.forEach(function (filter) {
@@ -138,7 +150,10 @@ Directive.prototype.applyFilters = function (value) {
 
 module.exports = {
 
-    // make sure the directive and value is valid
+    /*
+     *  make sure the directive and expression is valid
+     *  before we create an instance
+     */
     parse: function (dirname, expression) {
 
         var prefix = config.prefix

+ 11 - 0
src/directives/each.js

@@ -73,13 +73,16 @@ module.exports = {
         var ctn = this.container = this.el.parentNode
         this.marker = document.createComment('sd-each-' + this.arg)
         ctn.insertBefore(this.marker, this.el)
+        this.delegator = this.el.parentNode
         ctn.removeChild(this.el)
     },
 
     update: function (collection) {
         this.unbind(true)
+        // for event delegation
         if (!Array.isArray(collection)) return
         this.collection = collection
+        this.delegator.sdDelegationHandlers = {}
         var self = this
         collection.on('mutate', function (mutation) {
             mutationHandlers[mutation.method].call(self, mutation)
@@ -118,5 +121,13 @@ module.exports = {
             })
             this.collection = null
         }
+        var delegator = this.delegator
+        if (!delegator) return
+        var handlers = delegator.sdDelegationHandlers
+        for (var key in handlers) {
+            console.log('remove: ' + key)
+            delegator.removeEventListener(handlers[key].event, handlers[key])
+        }
+        delete delegator.sdDelegationHandlers
     }
 }

+ 22 - 28
src/directives/on.js

@@ -1,5 +1,3 @@
-// sniff matchesSelector() method name.
-
 var matches = 'atchesSelector',
     prefixes = ['m', 'webkitM', 'mozM', 'msM']
 
@@ -11,13 +9,13 @@ prefixes.some(function (prefix) {
     }
 })
 
-function delegateCheck (current, top, selector) {
-    if (current.webkitMatchesSelector(selector)) {
+function delegateCheck (current, top, marker) {
+    if (current[marker]) {
         return current
     } else if (current === top) {
         return false
     } else {
-        return delegateCheck(current.parentNode, top, selector)
+        return delegateCheck(current.parentNode, top, marker)
     }
 }
 
@@ -27,8 +25,8 @@ module.exports = {
 
     bind: function () {
         if (this.seed.each) {
-            this.selector = '[' + this.directiveName + '*="' + this.expression + '"]'
-            this.delegator = this.seed.el.parentNode
+            this.el[this.expression] = true
+            this.el.seed = this.seed
         }
     },
 
@@ -36,50 +34,46 @@ module.exports = {
         this.unbind()
         if (!handler) return
         var self  = this,
-            event = this.arg,
-            selector  = this.selector,
-            delegator = this.delegator
-        if (delegator) {
-
+            event = this.arg
+        if (this.seed.each && event !== 'blur') {
             // for each blocks, delegate for better performance
-            if (!delegator[selector]) {
-                console.log('binding listener')
-                delegator[selector] = function (e) {
-                    var target = delegateCheck(e.target, delegator, selector)
+            // blur events dont bubble so exclude them
+            var delegator = this.seed.el.parentNode
+            if (!delegator) return
+            var marker    = this.expression,
+                dHandler  = delegator.sdDelegationHandlers[marker]
+            // this only gets run once!!!
+            if (!dHandler) {
+                dHandler = delegator.sdDelegationHandlers[marker] = function (e) {
+                    var target = delegateCheck(e.target, delegator, marker)
                     if (target) {
-                        handler.call(self.seed.scope, {
+                        handler({
                             originalEvent : e,
                             el            : target,
                             scope         : target.seed.scope
                         })
                     }
                 }
-                delegator.addEventListener(event, delegator[selector])
+                dHandler.event = event
+                delegator.addEventListener(event, dHandler)
             }
 
         } else {
-
             // a normal handler
             this.handler = function (e) {
-                handler.call(self.seed.scope, {
+                handler({
                     originalEvent : e,
                     el            : e.currentTarget,
                     scope         : self.seed.scope
                 })
             }
             this.el.addEventListener(event, this.handler)
-
         }
     },
 
     unbind: function () {
-        var event = this.arg,
-            selector  = this.selector,
-            delegator = this.delegator
-        if (delegator && delegator[selector]) {
-            delegator.removeEventListener(event, delegator[selector])
-            delete delegator[selector]
-        } else if (this.handler) {
+        var event = this.arg
+        if (this.handler) {
             this.el.removeEventListener(event, this.handler)
         }
     }

+ 21 - 9
src/main.js

@@ -7,8 +7,10 @@ var controllers = config.controllers,
     datum       = config.datum,
     api         = {}
 
-// API
-
+/*
+ *  Store a piece of plain data in config.datum
+ *  so it can be consumed by sd-data
+ */
 api.data = function (id, data) {
     if (!data) return datum[id]
     if (datum[id]) {
@@ -17,6 +19,10 @@ api.data = function (id, data) {
     datum[id] = data
 }
 
+/*
+ *  Store a controller function in config.controllers
+ *  so it can be consumed by sd-controller
+ */
 api.controller = function (id, extensions) {
     if (!extensions) return controllers[id]
     if (controllers[id]) {
@@ -25,32 +31,38 @@ api.controller = function (id, extensions) {
     controllers[id] = extensions
 }
 
+/*
+ *  Allows user to create a custom directive
+ */
 api.directive = function (name, fn) {
     if (!fn) return directives[name]
     directives[name] = fn
 }
 
+/*
+ *  Allows user to create a custom filter
+ */
 api.filter = function (name, fn) {
     if (!fn) return filters[name]
     filters[name] = fn
 }
 
+/*
+ *  Bootstrap the whole thing
+ *  by creating a Seed instance for top level nodes
+ *  that has either sd-controller or sd-data
+ */
 api.bootstrap = function (opts) {
     if (opts) {
         config.prefix = opts.prefix || config.prefix
     }
-    var app = {}, n = 0, el, seed,
+    var el,
         ctrlSlt = '[' + config.prefix + '-controller]',
         dataSlt = '[' + config.prefix + '-data]'
     /* jshint boss: true */
     while (el = document.querySelector(ctrlSlt) || document.querySelector(dataSlt)) {
-        seed = new Seed(el)
-        if (el.id) {
-            app['$' + el.id] = seed
-        }
-        n++
+        new Seed(el)
     }
-    return n > 1 ? app : seed
 }
 
 module.exports = api

+ 78 - 66
src/seed.js

@@ -31,21 +31,21 @@ function Seed (el, options) {
         this[op] = options[op]
     }
 
-    // initialize the scope object
-    var dataPrefix = config.prefix + '-data'
-    var scope = this.scope =
-            (options && options.data)
-            || config.datum[el.getAttribute(dataPrefix)]
-            || {}
-    el.removeAttribute(dataPrefix)
-
-    // if the passed in data is already consumed by
-    // a Seed instance, make a copy from it
-    if (scope.$seed) {
-        scope = this.scope = scope.$dump()
+    // check if there's passed in data
+    var dataAttr = config.prefix + '-data',
+        dataId = el.getAttribute(dataAttr),
+        data = (options && options.data) || config.datum[dataId]
+    el.removeAttribute(dataAttr)
+
+    // if the passed in data is the scope of a Seed instance,
+    // make a copy from it
+    if (data && data.$seed instanceof Seed) {
+        data = data.$dump()
     }
 
-    // expose some useful stuff on the scope
+    // initialize the scope object
+    var scope = this.scope = {}
+    scope.$el       = el
     scope.$seed     = this
     scope.$destroy  = this._destroy.bind(this)
     scope.$dump     = this._dump.bind(this)
@@ -59,6 +59,13 @@ function Seed (el, options) {
     // now parse the DOM
     this._compileNode(el, true)
 
+    // copy data
+    if (data) {
+        for (var key in data) {
+            scope[key] = data[key]
+        }
+    }
+
     // if has controller function, apply it
     var ctrlID = el.getAttribute(ctrlAttr)
     if (ctrlID) {
@@ -79,7 +86,7 @@ function Seed (el, options) {
 }
 
 /*
- *  Compile a node (recursive)
+ *  Compile a DOM node (recursive)
  */
 Seed.prototype._compileNode = function (node, root) {
     var seed = this
@@ -103,13 +110,10 @@ Seed.prototype._compileNode = function (node, root) {
 
         } else if (ctrlExp && !root) { // nested controllers
 
-            var child = new Seed(node, {
+            new Seed(node, {
                 child: true,
                 parentSeed: seed
             })
-            if (node.id) {
-                seed['$' + node.id] = child
-            }
 
         } else { // normal node
 
@@ -155,22 +159,18 @@ Seed.prototype._bind = function (directive) {
     directive.seed = this
 
     var key = directive.key,
-        epr = this.eachPrefixRE,
-        isEachKey = epr && epr.test(key),
-        scope = this
-
-    if (isEachKey) {
-        key = directive.key = key.replace(epr, '')
-    }
+        seed = this
 
-    if (epr && !isEachKey) {
-        scope = this.parentSeed
+    if (this.each) {
+        if (this.eachPrefixRE.test(key)) {
+            key = directive.key = key.replace(this.eachPrefixRE, '')
+        } else {
+            seed = this.parentSeed
+        }
     }
 
-    var ownerScope = determinScope(directive, scope),
-        binding =
-            ownerScope._bindings[key] ||
-            ownerScope._createBinding(key)
+    var ownerScope = determinScope(directive, seed),
+        binding = ownerScope._bindings[key] || ownerScope._createBinding(key)
 
     // add directive to this binding
     binding.instances.push(directive)
@@ -180,18 +180,16 @@ Seed.prototype._bind = function (directive) {
     if (directive.bind) {
         directive.bind(binding.value)
     }
-
-    // set initial value
-    directive.update(binding.value)
-
 }
 
+/*
+ *  Create binding and attach getter/setter for a key to the scope object
+ */
 Seed.prototype._createBinding = function (key) {
 
-    var binding = new Binding(this.scope[key])
+    var binding = new Binding()
     this._bindings[key] = binding
 
-    // bind accessor triggers to scope
     var seed = this
     Object.defineProperty(this.scope, key, {
         get: function () {
@@ -200,7 +198,7 @@ Seed.prototype._createBinding = function (key) {
             }
             seed.emit('get', key)
             return binding.isComputed
-                ? binding.value()
+                ? binding.value.get()
                 : binding.value
         },
         set: function (value) {
@@ -212,6 +210,10 @@ Seed.prototype._createBinding = function (key) {
     return binding
 }
 
+/*
+ *  Update a binding with a new value.
+ *  Triggered in the binding's setter.
+ */
 Seed.prototype._updateBinding = function (key, value) {
 
     var binding = this._bindings[key],
@@ -222,7 +224,6 @@ Seed.prototype._updateBinding = function (key, value) {
         if (value.get) { // computed property
             this._computed.push(binding)
             binding.isComputed = true
-            value = value.get
         } else { // normal object
             // TODO watchObject
         }
@@ -233,17 +234,13 @@ Seed.prototype._updateBinding = function (key, value) {
         })
     }
 
-    binding.value = value
-
-    // update all instances
-    binding.instances.forEach(function (instance) {
-        instance.update(value)
-    })
-
-    // notify dependents to refresh themselves
-    binding.emitChange()
+    binding.update(value)
 }
 
+/*
+ *  Auto-extract the dependencies of a computed property
+ *  by recording the getters triggered when evaluating it
+ */
 Seed.prototype._parseDeps = function (binding) {
     depsObserver.on('get', function (dep) {
         if (!dep.dependents) {
@@ -251,10 +248,14 @@ Seed.prototype._parseDeps = function (binding) {
         }
         dep.dependents.push.apply(dep.dependents, binding.instances)
     })
-    binding.value()
+    binding.value.get()
     depsObserver.off('get')
 }
 
+/*
+ *  Call unbind() of all directive instances
+ *  to remove event listeners, destroy child seeds, etc.
+ */
 Seed.prototype._unbind = function () {
     var unbind = function (instance) {
         if (instance.unbind) {
@@ -266,15 +267,17 @@ Seed.prototype._unbind = function () {
     }
 }
 
+/*
+ *  Unbind and remove element
+ */
 Seed.prototype._destroy = function () {
     this._unbind()
-    delete this.el.seed
     this.el.parentNode.removeChild(this.el)
-    if (this.parentSeed && this.id) {
-        delete this.parentSeed['$' + this.id]
-    }
 }
 
+/*
+ *  Dump a copy of current scope data, excluding seed-exposed properties.
+ */
 Seed.prototype._dump = function () {
     var dump = {}, binding, val,
         subDump = function (scope) {
@@ -289,7 +292,7 @@ Seed.prototype._dump = function () {
         } else if (typeof val !== 'function') {
             dump[key] = val
         } else if (binding.isComputed) {
-            dump[key] = val()
+            dump[key] = val.get()
         }
     }
     return dump
@@ -298,12 +301,24 @@ Seed.prototype._dump = function () {
 /*
  *  Binding class
  */
- function Binding (value) {
-    this.value = value
+ function Binding () {
+    this.value = undefined
     this.instances = []
     this.dependents = []
  }
 
+ Binding.prototype.update = function (value) {
+     if (value === undefined) {
+        value = this.value
+    } else {
+        this.value = value
+    }
+     this.instances.forEach(function (instance) {
+         instance.update(value)
+     })
+     this.emitChange()
+ }
+
  Binding.prototype.emitChange = function () {
      this.dependents.forEach(function (dept) {
          dept.refresh()
@@ -313,25 +328,23 @@ Seed.prototype._dump = function () {
 // Helpers --------------------------------------------------------------------
 
 /*
- *  determinScope()
  *  determine which scope a key belongs to based on nesting symbols
  */
-function determinScope (key, scope) {
+function determinScope (key, seed) {
     if (key.nesting) {
         var levels = key.nesting
-        while (scope.parentSeed && levels--) {
-            scope = scope.parentSeed
+        while (seed.parentSeed && levels--) {
+            seed = seed.parentSeed
         }
     } else if (key.root) {
-        while (scope.parentSeed) {
-            scope = scope.parentSeed
+        while (seed.parentSeed) {
+            seed = seed.parentSeed
         }
     }
-    return scope
+    return seed
 }
 
-/* 
- *  typeOf()
+/*
  *  get accurate type of an object
  */
 var OtoString = Object.prototype.toString
@@ -340,7 +353,6 @@ function typeOf (obj) {
 }
 
 /*
- *  watchArray()
  *  augment an Array so that it emit events when mutated
  */
 var arrayMutators = ['push','pop','shift','unshift','splice','sort','reverse']