Evan You 12 лет назад
Родитель
Сommit
b23c790fbe
4 измененных файлов с 320 добавлено и 31 удалено
  1. 1 1
      README.md
  2. 1 0
      src/binding.js
  3. 49 30
      src/directive.js
  4. 269 0
      test/unit/directive.js

+ 1 - 1
README.md

@@ -45,7 +45,7 @@ Built versions in `/dist` or installed via Bower can be used directly as a Commo
 
 **Standalone**
 
-Simply include a built version in `/dist` or installed via Bower with a script tag. `seed` will be registered as a global variable.
+Simply include a built version in `/dist` or installed via Bower with a script tag. `seed` will be registered as a global variable. You can also use it directly over [Browserify CDN](http://wzrd.in) at [http://wzrd.in/standalone/seed-mvvm](http://wzrd.in/standalone/seed-mvvm)
 
 ## [ Docs under construction... ]
 

+ 1 - 0
src/binding.js

@@ -39,6 +39,7 @@ BindingProto.refresh = function () {
     while (i--) {
         this.instances[i].refresh()
     }
+    this.pub()
 }
 
 /*

+ 49 - 30
src/directive.js

@@ -3,9 +3,9 @@ var config     = require('./config'),
     directives = require('./directives'),
     filters    = require('./filters')
 
-var KEY_RE          = /^[^\|<]+/,
+var KEY_RE          = /^[^\|]+/,
     ARG_RE          = /([^:]+):(.+)$/,
-    FILTERS_RE      = /[^\|]\|[^\|<]+/g,
+    FILTERS_RE      = /\|[^\|]+/g,
     FILTER_TOKEN_RE = /[^\s']+|'[^']+'/g,
     NESTING_RE      = /^\^+/,
     SINGLE_VAR_RE   = /^[\w\.]+$/
@@ -14,7 +14,7 @@ var KEY_RE          = /^[^\|<]+/,
  *  Directive class
  *  represents a single directive instance in the DOM
  */
-function Directive (directiveName, expression) {
+function Directive (directiveName, expression, rawKey) {
 
     var definition = directives[directiveName]
 
@@ -33,15 +33,24 @@ function Directive (directiveName, expression) {
 
     this.directiveName = directiveName
     this.expression    = expression.trim()
-    this.rawKey        = expression.match(KEY_RE)[0].trim()
+    this.rawKey        = rawKey
     
-    this.parseKey(this.rawKey)
+    parseKey(this, rawKey)
+
     this.isExp = !SINGLE_VAR_RE.test(this.key)
     
     var filterExps = expression.match(FILTERS_RE)
-    this.filters = filterExps
-        ? filterExps.map(parseFilter)
-        : null
+    if (filterExps) {
+        this.filters = []
+        var i = 0, l = filterExps.length, filter
+        for (; i < l; i++) {
+            filter = parseFilter(filterExps[i])
+            if (filter) this.filters.push(filter)
+        }
+        if (!this.filters.length) this.filters = null
+    } else {
+        this.filters = null
+    }
 }
 
 var DirProto = Directive.prototype
@@ -49,7 +58,7 @@ var DirProto = Directive.prototype
 /*
  *  parse a key, extract argument and nesting/root info
  */
-DirProto.parseKey = function (rawKey) {
+function parseKey (dir, rawKey) {
 
     var argMatch = rawKey.match(ARG_RE)
 
@@ -57,41 +66,47 @@ DirProto.parseKey = function (rawKey) {
         ? argMatch[2].trim()
         : rawKey.trim()
 
-    this.arg = argMatch
+    dir.arg = argMatch
         ? argMatch[1].trim()
         : null
 
     var nesting = key.match(NESTING_RE)
-    this.nesting = nesting
+    dir.nesting = nesting
         ? nesting[0].length
         : false
 
-    this.root = key.charAt(0) === '$'
+    dir.root = key.charAt(0) === '$'
 
-    if (this.nesting) {
+    if (dir.nesting) {
         key = key.replace(NESTING_RE, '')
-    } else if (this.root) {
+    } else if (dir.root) {
         key = key.slice(1)
     }
 
-    this.key = key
+    dir.key = key
 }
 
-
 /*
  *  parse a filter expression
  */
 function parseFilter (filter) {
 
-    var tokens = filter.slice(2)
-        .match(FILTER_TOKEN_RE)
-        .map(function (token) {
-            return token.replace(/'/g, '').trim()
-        })
+    var tokens = filter.slice(1).match(FILTER_TOKEN_RE)
+    if (!tokens) return
+    tokens = tokens.map(function (token) {
+        return token.replace(/'/g, '').trim()
+    })
+
+    var name = tokens[0],
+        apply = filters[name]
+    if (!apply) {
+        utils.warn('Unknown filter: ' + name)
+        return
+    }
 
     return {
-        name  : tokens[0],
-        apply : filters[tokens[0]],
+        name  : name,
+        apply : apply,
         args  : tokens.length > 1
                 ? tokens.slice(1)
                 : null
@@ -124,7 +139,6 @@ DirProto.refresh = function (value) {
     if (value && value === this.computedValue) return
     this.computedValue = value
     this.apply(value)
-    this.binding.pub()
 }
 
 /*
@@ -145,7 +159,6 @@ DirProto.applyFilters = function (value) {
     var filtered = value, filter
     for (var i = 0, l = this.filters.length; i < l; i++) {
         filter = this.filters[i]
-        if (!filter.apply) utils.warn('Unknown filter: ' + filter.name)
         filtered = filter.apply(filtered, filter.args)
     }
     return filtered
@@ -153,8 +166,13 @@ DirProto.applyFilters = function (value) {
 
 /*
  *  Unbind diretive
+ *  @ param {Boolean} update
+ *    Sometimes we call unbind before an update (i.e. not destroy)
+ *    just to teardown previousstuff, in that case we do not want
+ *    to null everything.
  */
 DirProto.unbind = function (update) {
+    // this can be called before the el is even assigned...
     if (!this.el) return
     if (this._unbind) this._unbind(update)
     if (!update) this.vm = this.el = this.binding = this.compiler = null
@@ -170,14 +188,15 @@ Directive.parse = function (dirname, expression) {
     if (dirname.indexOf(prefix) === -1) return null
     dirname = dirname.slice(prefix.length + 1)
 
-    var dir   = directives[dirname],
-        valid = KEY_RE.test(expression)
+    var dir = directives[dirname],
+        keyMatch = expression.match(KEY_RE),
+        rawKey = keyMatch && keyMatch[0].trim()
 
     if (!dir) utils.warn('unknown directive: ' + dirname)
-    if (!valid) utils.warn('invalid directive expression: ' + expression)
+    if (!rawKey) utils.warn('invalid directive expression: ' + expression)
 
-    return dir && valid
-        ? new Directive(dirname, expression)
+    return dir && rawKey
+        ? new Directive(dirname, expression, rawKey)
         : null
 }
 

+ 269 - 0
test/unit/directive.js

@@ -0,0 +1,269 @@
+var assert = require('assert'),
+    Directive = require('../../src/directive'),
+    directives = require('../../src/directives')
+
+describe('UNIT: Directive', function () {
+
+    describe('.parse()', function () {
+        
+        it('should return null if directive name does not have correct prefix', function () {
+            var d = Directive.parse('ds-test', 'abc')
+            assert.ok(d === null)
+        })
+
+        it('should return null if directive is unknown', function () {
+            var d = Directive.parse('sd-directive-that-does-not-exist', 'abc')
+            assert.ok(d === null)
+        })
+
+        it('should return null if the expression is invalid', function () {
+            var d = Directive.parse('sd-text', ''),
+                e = Directive.parse('sd-text', '  '),
+                f = Directive.parse('sd-text', '|'),
+                g = Directive.parse('sd-text', '  |  ')
+            assert.ok(d === null, 'empty')
+            assert.ok(e === null, 'spaces')
+            assert.ok(f === null, 'single pipe')
+            assert.ok(g === null, 'pipe with spaces')
+        })
+
+        it('should return an instance of Directive if args are good', function () {
+            var d = Directive.parse('sd-text', 'abc')
+            assert.ok(d instanceof Directive)
+        })
+
+    })
+
+    describe('instantiation', function () {
+
+        var test = function () {}
+        directives.test = test
+
+        var obj = {
+            bind: function () {},
+            update: function () {},
+            unbind: function () {},
+            custom: function () {}
+        }
+        directives.obj = obj
+        
+        it('should copy the definition as _update if the def is a function', function () {
+            var d = Directive.parse('sd-test', 'abc')
+            assert.ok(d._update === test)                
+        })
+
+        it('should copy methods if the def is an object', function () {
+            var d = Directive.parse('sd-obj', 'abc')
+            assert.ok(d._update === obj.update, 'update should be copied as _update')
+            assert.ok(d._unbind === obj.unbind, 'unbind should be copied as _unbind')
+            assert.ok(d.bind === obj.bind)
+            assert.ok(d.custom === obj.custom, 'should copy any custom methods')
+        })
+
+        it('should trim the expression', function () {
+            var exp = ' fsfsef   | fsef a  ',
+                d = Directive.parse('sd-text', exp)
+            assert.ok(d.expression === exp.trim())
+        })
+
+        it('should extract correct argument', function () {
+            var d = Directive.parse('sd-text', 'todo:todos'),
+                e = Directive.parse('sd-text', 'todo:todos + abc'),
+                f = Directive.parse('sd-text', 'todo:todos | fsf fsef')
+            assert.ok(d.arg === 'todo', 'simple')
+            assert.ok(e.arg === 'todo', 'expression')
+            assert.ok(f.arg === 'todo', 'with filters')
+        })
+
+        it('should extract correct nesting info', function () {
+            var d = Directive.parse('sd-text', 'abc'),
+                e = Directive.parse('sd-text', '^abc'),
+                f = Directive.parse('sd-text', '^^^abc'),
+                g = Directive.parse('sd-text', '$abc')
+            assert.ok(d.nesting === false && d.root === false && d.key === 'abc', 'no nesting')
+            assert.ok(e.nesting === 1 && e.root === false && e.key === 'abc', '1 level')
+            assert.ok(f.nesting === 3 && f.root === false && f.key === 'abc', '3 levels')
+            assert.ok(g.root === true && g.nesting === false && g.key === 'abc', 'root')
+        })
+
+        it('should be able to determine whether the key is an expression', function () {
+            var d = Directive.parse('sd-text', 'abc'),
+                e = Directive.parse('sd-text', '!abc'),
+                f = Directive.parse('sd-text', 'abc + bcd * 5 / 2'),
+                g = Directive.parse('sd-text', 'abc && (bcd || eee)'),
+                h = Directive.parse('sd-text', 'test(abc)')
+            assert.ok(d.isExp === false)
+            assert.ok(e.isExp, 'negation')
+            assert.ok(f.isExp, 'math')
+            assert.ok(g.isExp, 'logic')
+            assert.ok(g.isExp, 'function invocation')
+        })
+
+        it('should have a filter prop of null if no filters are present', function () {
+            var d = Directive.parse('sd-text', 'abc'),
+                e = Directive.parse('sd-text', 'abc |'),
+                f = Directive.parse('sd-text', 'abc ||'),
+                g = Directive.parse('sd-text', 'abc | | '),
+                h = Directive.parse('sd-text', 'abc | unknown | nothing at all | whaaat')
+            assert.ok(d.filters === null)
+            assert.ok(e.filters === null, 'single')
+            assert.ok(f.filters === null, 'double')
+            assert.ok(g.filters === null, 'with spaces')
+            assert.ok(h.filters === null, 'with unknown filters')
+        })
+
+        it('should extract correct filters (single filter)', function () {
+            var d = Directive.parse('sd-text', 'abc | uppercase'),
+                f = d.filters[0]
+            assert.ok(f.name === 'uppercase' && f.args === null)
+            assert.ok(f.apply('test') === 'TEST')
+        })
+
+        it('should extract correct filters (single filter with args)', function () {
+            var d = Directive.parse('sd-text', 'abc | pluralize item \'arg with spaces\''),
+                f = d.filters[0]
+            assert.ok(f.name === 'pluralize', 'name')
+            assert.ok(f.args.length === 2, 'args length')
+            assert.ok(f.args[0] === 'item' && f.args[1] === 'arg with spaces', 'args value')
+        })
+
+        it('should extract correct filters (multiple filters)', function () {
+            // intentional double pipe
+            var d = Directive.parse('sd-text', 'abc | uppercase | pluralize item || lowercase'),
+                f1 = d.filters[0],
+                f2 = d.filters[1],
+                f3 = d.filters[2]
+            assert.ok(d.filters.length === 3)
+            assert.ok(f1.name === 'uppercase')
+            assert.ok(f2.name === 'pluralize' && f2.args[0] === 'item')
+            assert.ok(f3.name === 'lowercase')
+        })
+
+    })
+
+    describe('.applyFilters()', function () {
+        
+        it('should work', function () {
+            var d = Directive.parse('sd-text', 'abc | pluralize item | capitalize'),
+                v = d.applyFilters(2)
+            assert.ok(v === 'Items')
+        })
+
+    })
+
+    describe('.apply()', function () {
+
+        var test,
+            applyTest = function (val) { test = val }
+        directives.applyTest = applyTest
+
+        it('should invole the _update function', function () {
+            var d = Directive.parse('sd-applyTest', 'abc')
+            d.apply(12345)
+            assert.ok(test === 12345)
+        })
+        
+        it('should apply the filter if there is any', function () {
+            var d = Directive.parse('sd-applyTest', 'abc | currency £')
+            d.apply(12345)
+            assert.ok(test === '£123,45.00')
+        })
+
+    })
+
+    describe('.update()', function () {
+        
+        var d = Directive.parse('sd-text', 'abc'),
+            applied = false
+        d.apply = function () {
+            applied = true
+        }
+
+        it('should apply() for first time update, even if the value is undefined', function () {
+            d.update(undefined, true)
+            assert.ok(applied === true)
+        })
+
+        it('should apply() when a different value is given', function () {
+            applied = false
+            d.update(123)
+            assert.ok(d.value === 123 && applied === true)
+        })
+
+        it('should not apply() if the value is the same', function () {
+            applied = false
+            d.update(123)
+            assert.ok(!applied)
+        })
+
+    })
+
+    describe('.refresh()', function () {
+        
+        var d = Directive.parse('sd-text', 'abc'),
+            applied = false,
+            el = 1, vm = 2,
+            value = {
+                get: function (ctx) {
+                    return ctx.el + ctx.vm
+                }
+            }
+        d.el = el
+        d.vm = vm
+        d.apply = function () {
+            applied = true
+        }
+
+        d.refresh(value)
+
+        it('should set the value if value arg is given', function () {
+            assert.ok(d.value === value)
+        })
+
+        it('should get its el&vm context and get correct computedValue', function () {
+            assert.ok(d.computedValue === el + vm)
+        })
+
+        it('should call apply()', function () {
+            assert.ok(applied)
+        })
+
+        it('should not call apply() if computedValue is the same', function () {
+            applied = false
+            d.refresh()
+            assert.ok(!applied)
+        })
+
+    })
+
+    describe('.unbind()', function () {
+        
+        var d = Directive.parse('sd-text', 'abc'),
+            unbound = false,
+            val
+        d._unbind = function (v) {
+            val = v
+            unbound = true
+        }
+
+        it('should not work if it has no element yet', function () {
+            d.unbind()
+            assert.ok(unbound === false)
+        })
+
+        it('should call _unbind() if it has an element', function () {
+            d.el = true
+            d.unbind(true)
+            assert.ok(unbound === true)
+            // should not null everything unless it's an update
+            assert.ok(d.el && d.vm)  
+        })
+
+        it('should null everything if it\'s called for VM destruction', function () {
+            d.unbind()
+            assert.ok(d.el === null && d.vm === null && d.binding === null && d.compiler === null)
+        })
+
+    })
+
+})