Jelajahi Sumber

big refactor.. again

Evan You 13 tahun lalu
induk
melakukan
23a2bde889
12 mengubah file dengan 258 tambahan dan 168 penghapusan
  1. 4 3
      TODO.md
  2. 3 1
      component.json
  3. 47 30
      dev.html
  4. 95 0
      src/binding.js
  5. 1 0
      src/controllers.js
  6. 0 88
      src/directive.js
  7. 8 3
      src/directives.js
  8. 12 1
      src/filters.js
  9. 37 4
      src/main.js
  10. 49 36
      src/seed.js
  11. 0 0
      src/textNodeParser.js
  12. 2 2
      test/test.js

+ 4 - 3
TODO.md

@@ -1,4 +1,5 @@
-- ? separate scope data and prototype methods // think about this
-- repeat directive by watching an array
+- fix event delegation
+- improve arrayWatcher
 - make Seeds compositable
-- parse textNodes
+- parse textNodes
+- computed properties

+ 3 - 1
component.json

@@ -9,9 +9,11 @@
     "src/main.js",
     "src/config.js",
     "src/seed.js",
-    "src/directive.js",
+    "src/binding.js",
+    "src/textNodeParser.js",
     "src/directives.js",
     "src/filters.js",
+    "src/controllers.js",
     "src/watchArray.js"
   ]
 }

+ 47 - 30
dev.html

@@ -14,15 +14,20 @@
 		</style>
 	</head>
 	<body>
-		<div id="test" sd-on-click="changeMessage | delegate .button">
-            <p sd-text="msg.wow | capitalize" sd-on-click="remove"></p>
-            <p sd-text="msg.wow | uppercase" class="button"></p>
+		<div id="app" sd-controller="TodoList">
+            <p sd-text="msg | capitalize" sd-on="click:changeMessage"></p>
+            <p sd-text="msg | uppercase"></p>
+            <p sd-on="click:remove">bye</p>
             <p sd-text="total | money"></p>
-            <p sd-class-red="error" sd-text="hello"></p>
+            <p sd-class="red:error" sd-show="error">Error</p>
             <ul sd-show="todos">
-            	<li sd-each-todo="todos">
-                    <span class="todo" sd-text="todo.title" sd-class-done="todo.done"></span>   
-                </li>
+            	<li class="todo"
+                    sd-controller="Todo"
+                    sd-each="todo:todos"
+                    sd-class="done:todo.done"
+                    sd-on="click:todo.toggle"
+                    sd-text="todo.title"
+                ></li>
             </ul>
         </div>
 		<script>
@@ -30,36 +35,48 @@
 			var Seed = require('seed')
 
 			Seed.filter('money', function (value) {
-			    return '$' + value.toFixed(2)
+			    return value
+                    ? '$' + value.toFixed(2)
+                    : ''
 			})
 
-            var list = [
-                {
-                    title: 'make this shit kinda work',
-                    done: true
-                },
-                {
-                    title: 'make this shit work',
-                    done: false
+            Seed.controller('TodoList', {
+                changeMessage: function () {
+                    this.scope.msg = 'It works!'
                 },
-                {
-                    title: 'more features!!!',
-                    done: false
+                remove: function () {
+                    this.destroy()
+                }
+            })
+
+            Seed.controller('Todo', {
+                toggle: function () {
+                    this.scope.done = !scope.done
                 }
-            ]
+            })
 
             var s = Date.now()
 
-			var todos = new Seed('#test', {
-                total     : Math.random() * 100000,
-                'msg.wow' : 'wow',
-                hello     : 'hello',
-                todos     : list,
-                changeMessage: function () {
-                    this.scope['msg.wow'] = 'hola'
-                },
-                remove: function () {
-                    this.destroy()
+			var app = Seed.bootstrap({
+                el: '#app',
+                data: {
+                    msg: 'hello!',
+                    total: 9999,
+                    error: true,
+                    todos: [
+                        {
+                            title: 'hello!',
+                            done: true
+                        },
+                        {
+                            title: 'hello!!',
+                            done: false
+                        },
+                        {
+                            title: 'hello!!!',
+                            done: false
+                        }
+                    ]
                 }
             })
 

+ 95 - 0
src/binding.js

@@ -0,0 +1,95 @@
+var config     = require('./config'),
+    directives = require('./directives'),
+    filters    = require('./filters')
+
+var KEY_RE          = /^[^\|]+/,
+    ARG_RE          = /([^:]+):(.+)$/,
+    FILTERS_RE      = /\|[^\|]+/g,
+    FILTER_TOKEN_RE = /[^\s']+|'[^']+'/g,
+    QUOTE_RE        = /'/g
+
+function Binding (directiveName, expression) {
+
+    var directive = directives[directiveName]
+    if (typeof directive === 'function') {
+        this._update = directive
+    } else {
+        for (var prop in directive) {
+            if (prop === 'update') {
+                this['_update'] = directive.update
+            } else {
+                this[prop] = directive[prop]
+            }
+        }
+    }
+    this.directiveName = directiveName
+
+    var rawKey   = expression.match(KEY_RE)[0], // guarded in parse
+        argMatch = rawKey.match(ARG_RE)
+
+    this.key = argMatch
+        ? argMatch[2].trim()
+        : rawKey.trim()
+
+    this.arg = argMatch
+        ? argMatch[1].trim()
+        : null
+    
+    var filterExpressions = expression.match(FILTERS_RE)
+    if (filterExpressions) {
+        this.filters = filterExpressions.map(function (filter) {
+            var tokens = filter.slice(1)
+                .match(FILTER_TOKEN_RE)
+                .map(function (token) {
+                    return token.replace(QUOTE_RE, '').trim()
+                })
+            return {
+                name  : tokens[0],
+                apply : filters[tokens[0]],
+                args  : tokens.length > 1
+                        ? tokens.slice(1)
+                        : null
+            }
+        })
+    } else {
+        this.filters = null
+    }
+}
+
+Binding.prototype.update = function (value) {
+    // apply filters
+    if (this.filters) {
+        value = this.applyFilters(value)
+    }
+    this._update(value)
+}
+
+Binding.prototype.applyFilters = function (value) {
+    var filtered = value
+    this.filters.forEach(function (filter) {
+        if (!filter.apply) throw new Error('Unknown filter: ' + filter.name)
+        filtered = filter.apply(filtered, filter.args)
+    })
+    return filtered
+}
+
+module.exports = {
+
+    // make sure the directive and value is valid
+    parse: function (dirname, expression) {
+
+        var prefix = config.prefix
+        if (dirname.indexOf(prefix) === -1) return null
+        dirname = dirname.slice(prefix.length + 1)
+
+        var dir   = directives[dirname],
+            valid = KEY_RE.test(expression)
+
+        if (!dir) console.warn('unknown directive: ' + dirname)
+        if (!valid) console.warn('invalid directive expression: ' + expression)
+
+        return dir && valid
+            ? new Binding(dirname, expression)
+            : null
+    }
+}

+ 1 - 0
src/controllers.js

@@ -0,0 +1 @@
+module.exports = {}

+ 0 - 88
src/directive.js

@@ -1,88 +0,0 @@
-var config     = require('./config'),
-    Directives = require('./directives'),
-    Filters    = require('./filters')
-
-var KEY_RE          = /^[^\|]+/,
-    FILTERS_RE      = /\|[^\|]+/g,
-    FILTER_TOKEN_RE = /[^\s']+|'[^']+'/g,
-    QUOTE_RE        = /'/g
-
-function Directive (def, attr, arg, key) {
-
-    if (typeof def === 'function') {
-        this._update = def
-    } else {
-        for (var prop in def) {
-            if (prop === 'update') {
-                this['_update'] = def.update
-                continue
-            }
-            this[prop] = def[prop]
-        }
-    }
-
-    this.attr = attr
-    this.arg  = arg
-    this.key  = key
-    
-    var filters = attr.value.match(FILTERS_RE)
-    if (filters) {
-        this.filters = filters.map(function (filter) {
-            var tokens = filter.slice(1)
-                .match(FILTER_TOKEN_RE)
-                .map(function (token) {
-                    return token.replace(QUOTE_RE, '').trim()
-                })
-            return {
-                name  : tokens[0],
-                apply : Filters[tokens[0]],
-                args  : tokens.length > 1
-                        ? tokens.slice(1)
-                        : null
-            }
-        })
-    }
-}
-
-Directive.prototype.update = function (value) {
-    // apply filters
-    if (this.filters) {
-        value = this.applyFilters(value)
-    }
-    this._update(value)
-}
-
-Directive.prototype.applyFilters = function (value) {
-    var filtered = value
-    this.filters.forEach(function (filter) {
-        if (!filter.apply) throw new Error('Unknown filter: ' + filter.name)
-        filtered = filter.apply(filtered, filter.args)
-    })
-    return filtered
-}
-
-module.exports = {
-
-    // make sure the directive and value is valid
-    parse: function (attr) {
-
-        var prefix = config.prefix
-        if (attr.name.indexOf(prefix) === -1) return null
-
-        var noprefix = attr.name.slice(prefix.length + 1),
-            argIndex = noprefix.indexOf('-'),
-            arg = argIndex === -1
-                ? null
-                : noprefix.slice(argIndex + 1),
-            name = arg
-                ? noprefix.slice(0, argIndex)
-                : noprefix,
-            def = Directives[name]
-
-        var key = attr.value.match(KEY_RE)
-
-        return def && key
-            ? new Directive(def, attr, arg, key[0].trim())
-            : null
-    }
-}

+ 8 - 3
src/directives.js

@@ -1,4 +1,5 @@
-var config     = require('./config'),
+var config = require('./config'),
+    controllers = require('./controllers'),
     watchArray = require('./watchArray')
 
 module.exports = {
@@ -66,8 +67,12 @@ module.exports = {
             console.log(mutation)
         },
         buildItem: function (data, index, collection) {
-            var node = this.el.cloneNode(true),
-                spore = new Seed(node, data, {
+            var Seed = require('./seed'),
+                node = this.el.cloneNode(true),
+                ctrl = node.getAttribute(config.prefix + '-controller'),
+                Ctrl = ctrl ? controllers[ctrl] : Seed
+            if (ctrl) node.removeAttribute(config.prefix + '-controller')
+            var spore = new Ctrl(node, data, {
                     eachPrefixRE: this.prefixRE,
                     parentScope: this.seed.scope
                 })

+ 12 - 1
src/filters.js

@@ -12,10 +12,21 @@ module.exports = {
     delegate: function (handler, args) {
         var selector = args[0]
         return function (e) {
-            if (e.target.webkitMatchesSelector(selector)) {
+            console.log('triggered')
+            if (delegateCheck(e.target, e.currentTarget, selector)) {
                 handler.apply(this, arguments)
             }
         }
     }
 
+}
+
+function delegateCheck (current, top, selector) {
+    if (current.webkitMatchesSelector(selector)) {
+        return true
+    } else if (current === top) {
+        return false
+    } else {
+        return delegateCheck(current.parentNode, top, selector)
+    }
 }

+ 37 - 4
src/main.js

@@ -1,7 +1,8 @@
-var config     = require('./config'),
-    Seed       = require('./seed'),
-    directives = require('./directives'),
-    filters    = require('./filters')
+var config      = require('./config'),
+    Seed        = require('./seed'),
+    directives  = require('./directives'),
+    filters     = require('./filters'),
+    controllers = require('./controllers')
 
 Seed.config = config
 
@@ -23,6 +24,34 @@ Seed.extend = function (opts) {
     return Spore
 }
 
+Seed.controller = function (id, extensions) {
+    if (controllers[id]) {
+        console.warn('controller "' + id + '" was already registered and has been overwritten.')
+    }
+    var c = controllers[id] = Seed.extend(extensions)
+    return c
+}
+
+Seed.bootstrap = function (seeds) {
+    if (!Array.isArray(seeds)) seeds = [seeds]
+    var instances = []
+    seeds.forEach(function (seed) {
+        var el = seed.el
+        if (typeof el === 'string') {
+            el = document.querySelector(el)
+        }
+        if (!el) console.warn('invalid element or selector: ' + seed.el)
+        var ctrlid = el.getAttribute(config.prefix + '-controller'),
+            Controller = ctrlid ? controllers[ctrlid] : Seed
+        if (!Controller) console.warn('controller ' + ctrlid + ' is not defined.')
+        if (ctrlid) el.removeAttribute(config.prefix + '-controller')
+        instances.push(new Controller(el, seed.data, seed.options))
+    })
+    return instances.length > 1
+        ? instances
+        : instances[0]
+}
+
 Seed.directive = function (name, fn) {
     directives[name] = fn
 }
@@ -31,4 +60,8 @@ Seed.filter = function (name, fn) {
     filters[name] = fn
 }
 
+// alias for an alternative API
+Seed.evolve = Seed.controller
+Seed.plant  = Seed.bootstrap
+
 module.exports = Seed

+ 49 - 36
src/seed.js

@@ -1,8 +1,8 @@
-var config      = require('./config'),
-    Directive   = require('./directive')
+var config = require('./config'),
+    bindingParser = require('./binding')
 
-var map  = Array.prototype.map,
-    each = Array.prototype.forEach
+var map    = Array.prototype.map,
+    each   = Array.prototype.forEach
 
 function Seed (el, data, options) {
 
@@ -11,22 +11,28 @@ function Seed (el, data, options) {
     }
 
     this.el         = el
-    this.scope      = {}
+    this.scope      = data
     this._bindings  = {}
     this._options   = options || {}
 
+    var key, dataCopy = {}
+    for (key in data) {
+        dataCopy[key] = data[key]
+    }
+
     // process nodes for directives
     this._compileNode(el)
 
     // initialize all variables by invoking setters
-    for (var key in this._bindings) {
-        this.scope[key] = data[key]
+    for (key in this._bindings) {
+        this.scope[key] = dataCopy[key]
     }
 
 }
 
 Seed.prototype._compileNode = function (node) {
-    var self = this
+    var self = this,
+        ctrl = config.prefix + '-controller'
 
     if (node.nodeType === 3) {
         // text node
@@ -36,14 +42,17 @@ Seed.prototype._compileNode = function (node) {
         var attrs = map.call(node.attributes, function (attr) {
             return {
                 name: attr.name,
-                value: attr.value
+                expressions: attr.value.split(',')
             }
         })
         attrs.forEach(function (attr) {
-            var directive = Directive.parse(attr)
-            if (directive) {
-                self._bind(node, directive)
-            }
+            if (attr.name === ctrl) return
+            attr.expressions.forEach(function (exp) {
+                var binding = bindingParser.parse(attr.name, exp)
+                if (binding) {
+                    self._bind(node, binding)
+                }
+            })
         })
     }
 
@@ -55,52 +64,56 @@ Seed.prototype._compileNode = function (node) {
 }
 
 Seed.prototype._compileTextNode = function (node) {
-    
+    return node
 }
 
-Seed.prototype._bind = function (node, directive) {
+Seed.prototype._bind = function (node, bindingInstance) {
 
-    directive.seed = this
-    directive.el = node
+    bindingInstance.seed = this
+    bindingInstance.el = node
 
-    node.removeAttribute(directive.attr.name)
+    node.removeAttribute(config.prefix + '-' + bindingInstance.directiveName)
 
-    var key = directive.key,
-        epr = this._options.eachPrefixRE
-    if (epr) {
+    var key = bindingInstance.key,
+        scope = this.scope,
+        epr = this._options.eachPrefixRE,
+        isEach = epr && epr.test(key)
+    // TODO make scope chain work on nested controllers
+    if (isEach) {
         key = key.replace(epr, '')
+        scope = this._options.parentScope
     }
 
-    var binding  = this._bindings[key] || this._createBinding(key)
+    var binding  = this._bindings[key] || this._createBinding(key, scope)
 
     // add directive to this binding
-    binding.directives.push(directive)
+    binding.instances.push(bindingInstance)
 
     // invoke bind hook if exists
-    if (directive.bind) {
-        directive.bind.call(directive, binding.value)
+    if (bindingInstance.bind) {
+        bindingInstance.bind(binding.value)
     }
 
 }
 
-Seed.prototype._createBinding = function (key) {
+Seed.prototype._createBinding = function (key, scope) {
 
     var binding = {
-        value: undefined,
-        directives: []
+        value: null,
+        instances: []
     }
 
     this._bindings[key] = binding
 
     // bind accessor triggers to scope
-    Object.defineProperty(this.scope, key, {
+    Object.defineProperty(scope, key, {
         get: function () {
             return binding.value
         },
         set: function (value) {
             binding.value = value
-            binding.directives.forEach(function (directive) {
-                directive.update(value)
+            binding.instances.forEach(function (instance) {
+                instance.update(value)
             })
         }
     })
@@ -118,13 +131,13 @@ Seed.prototype.dump = function () {
 
 Seed.prototype.destroy = function () {
     for (var key in this._bindings) {
-        this._bindings[key].directives.forEach(unbind)
-        delete this._bindings[key]
+        this._bindings[key].instances.forEach(unbind)
+        ;delete this._bindings[key]
     }
     this.el.parentNode.removeChild(this.el)
-    function unbind (directive) {
-        if (directive.unbind) {
-            directive.unbind()
+    function unbind (instance) {
+        if (instance.unbind) {
+            instance.unbind()
         }
     }
 }

+ 0 - 0
src/textNodeParser.js


+ 2 - 2
test/test.js

@@ -1,7 +1,7 @@
 var Seed = require('seed')
 
 describe('Seed', function () {
-    it('should have a create method', function () {
-        assert.ok(Seed.create)
+    it('should have a extend method', function () {
+        assert.ok(Seed.extend)
     })
 })