Pārlūkot izejas kodu

conditional components!

Evan You 12 gadi atpakaļ
vecāks
revīzija
2fe832f72b

+ 37 - 17
src/compiler.js

@@ -7,6 +7,7 @@ var Emitter     = require('./emitter'),
     TextParser  = require('./text-parser'),
     DepsParser  = require('./deps-parser'),
     ExpParser   = require('./exp-parser'),
+    ViewModel,
     
     // cache methods
     slice       = [].slice,
@@ -16,6 +17,8 @@ var Emitter     = require('./emitter'),
     def         = utils.defProtected,
     hasOwn      = ({}).hasOwnProperty,
 
+    SINGLE_VAR_RE = /^[\w\.$]+$/,
+
     // hooks to register
     hooks = [
         'created', 'ready',
@@ -337,12 +340,8 @@ CompilerProto.compile = function (node, root) {
         var repeatExp,
             withExp,
             directive,
-            componentId =
-                utils.attr(node, 'component') ||
-                (tagName.indexOf('-') > 0 && tagName.toLowerCase()),
-            componentCtor =
-                componentId &&
-                compiler.getOption('components', componentId)
+            // resolve a standalone child component with no inherited data
+            Ctor = this.resolveComponent(node, null, true)
 
         // It is important that we access these attributes
         // procedurally because the order matters.
@@ -358,21 +357,19 @@ CompilerProto.compile = function (node, root) {
             // repeat block cannot have v-id at the same time.
             directive = Directive.parse('repeat', repeatExp, compiler, node)
             if (directive) {
-                directive.Ctor = componentCtor
                 // defer child component compilation
                 // so by the time they are compiled, the parent
                 // would have collected all bindings
                 compiler.deferred.push(directive)
             }
 
-        // v-with has 2nd highest priority
-        } else if (root !== true && ((withExp = utils.attr(node, 'with')) || componentCtor)) {
+        // Child component has 2nd highest priority
+        } else if (root !== true && ((withExp = utils.attr(node, 'with')) || Ctor)) {
 
             withExp = Directive.split(withExp || '')
             withExp.forEach(function (exp, i) {
                 var directive = Directive.parse('with', exp, compiler, node)
                 if (directive) {
-                    directive.Ctor = componentCtor
                     // notify the directive that this is the
                     // last expression in the group
                     directive.last = i === withExp.length - 1
@@ -765,23 +762,31 @@ CompilerProto.removeListener = function (listener) {
 
 /**
  *  Do a one-time eval of a string that potentially
- *  includes bindings.
+ *  includes bindings. It accepts additional raw data
+ *  because we need to dynamically resolve v-component
+ *  before a childVM is even compiled...
+ *  TODO: make it less of a hack.
  */
-CompilerProto.eval = function (exp) {
+CompilerProto.eval = function (exp, data) {
     var tokens = TextParser.parse(exp)
     if (!tokens) { // no bindings
         return exp
     } else {
-        var token, getter,
+        var token,
             i = -1,
             l = tokens.length,
-            res = ''
+            res = '',
+            dataVal
         while (++i < l) {
             token = tokens[i]
             if (token.key) {
-                getter = ExpParser.parse(token.key, this)
-                if (getter) {
-                    res += utils.toText(getter.call(this.vm))
+                if (SINGLE_VAR_RE.test(token.key)) {
+                    dataVal = data && utils.get(data, token.key)
+                    res += dataVal === undefined
+                        ? utils.get(this.vm, token.key)
+                        : dataVal
+                } else {
+                    res += ExpParser.eval(token.key, this, data)
                 }
             } else {
                 res += token
@@ -791,6 +796,21 @@ CompilerProto.eval = function (exp) {
     }
 }
 
+/**
+ *  Resolve a Component constructor for an element
+ *  with the data to be used
+ */
+CompilerProto.resolveComponent = function (node, data, test) {
+    var exp     = utils.attr(node, 'component', test),
+        tagName = node.tagName,
+        id      = this.eval(exp, data) ||
+                  (tagName.indexOf('-') > 0 && tagName.toLowerCase()),
+        Ctor    = this.getOption('components', id)
+    return test
+        ? Ctor
+        : Ctor || (ViewModel || (ViewModel = require('./viewmodel')))
+}
+
 /**
  *  Unbind and remove element
  */

+ 8 - 36
src/config.js

@@ -1,38 +1,10 @@
-var prefix = 'v',
-    specialAttributes = [
-        'pre',
-        'ref',
-        'with',
-        'repeat',
-        'partial',
-        'component',
-        'animation',
-        'transition',
-        'effect'
-    ],
-    config = module.exports = {
+module.exports = {
 
-        debug          : false,
-        silent         : false,
-        enterClass     : 'v-enter',
-        leaveClass     : 'v-leave',
-        interpolate    : true,
-        attrs          : {},
+    prefix         : 'v',
+    debug          : false,
+    silent         : false,
+    enterClass     : 'v-enter',
+    leaveClass     : 'v-leave',
+    interpolate    : true
 
-        get prefix () {
-            return prefix
-        },
-        set prefix (val) {
-            prefix = val
-            updatePrefix()
-        }
-        
-    }
-
-function updatePrefix () {
-    specialAttributes.forEach(function (attr) {
-        config.attrs[attr] = prefix + '-' + attr
-    })
-}
-
-updatePrefix()
+}

+ 7 - 8
src/directives/repeat.js

@@ -1,8 +1,7 @@
 var Observer   = require('../observer'),
     utils      = require('../utils'),
     config     = require('../config'),
-    def        = utils.defProtected,
-    ViewModel // lazy def to avoid circular dependency
+    def        = utils.defProtected
 
 /**
  *  Mathods that perform precise DOM manipulation
@@ -76,9 +75,6 @@ module.exports = {
         var el   = this.el,
             ctn  = this.container = el.parentNode
 
-        // extract child VM information, if any
-        ViewModel = ViewModel || require('../viewmodel')
-        this.Ctor = this.Ctor || ViewModel
         // extract child Id, if any
         this.childId = this.compiler.eval(utils.attr(el, 'ref'))
 
@@ -197,7 +193,8 @@ module.exports = {
      *  Run a dry build just to collect bindings
      */
     dryBuild: function () {
-        new this.Ctor({
+        var Ctor = this.compiler.resolveComponent(this.el)
+        new Ctor({
             el     : this.el.cloneNode(true),
             parent : this.vm,
             compilerOptions: {
@@ -217,7 +214,7 @@ module.exports = {
         var self = this,
             ctn = self.container,
             vms = self.vms,
-            el, oldIndex, existing, item, nonObject
+            el, Ctor, oldIndex, existing, item, nonObject
 
         // get our DOM insertion reference node
         var ref = vms.length > index
@@ -260,8 +257,10 @@ module.exports = {
             // set index so vm can init with the correct
             // index instead of undefined
             data.$index = index
+            // resolve the constructor
+            Ctor = this.compiler.resolveComponent(el, data)
             // initialize the new VM
-            item = new self.Ctor({
+            item = new Ctor({
                 el     : el,
                 data   : data,
                 parent : self.vm,

+ 3 - 5
src/directives/with.js

@@ -1,5 +1,4 @@
-var ViewModel,
-    nextTick = require('../utils').nextTick
+var nextTick = require('../utils').nextTick
 
 module.exports = {
 
@@ -39,13 +38,12 @@ module.exports = {
     },
 
     build: function (value) {
-        ViewModel = ViewModel || require('../viewmodel')
-        var Ctor = this.Ctor || ViewModel,
-            data = value
+        var data = value
         if (this.arg) {
             data = {}
             data[this.arg] = value
         }
+        var Ctor = this.compiler.resolveComponent(this.el, data)
         this.subVM = new Ctor({
             el     : this.el,
             data   : data,

+ 25 - 3
src/exp-parser.js

@@ -53,10 +53,16 @@ function getVariables (code) {
  *  key. It then creates any missing bindings on the
  *  final resolved vm.
  */
-function getRel (path, compiler) {
+function getRel (path, compiler, data) {
     var rel  = '',
         dist = 0,
         self = compiler
+
+    if (data && utils.get(data, path) !== undefined) {
+        // hack: temporarily attached data
+        return '$temp.'
+    }
+
     while (compiler) {
         if (compiler.hasKey(path)) {
             break
@@ -107,7 +113,7 @@ function escapeDollar (v) {
  *  from an arbitrary expression, together with a list of paths to be
  *  created as bindings.
  */
-exports.parse = function (exp, compiler) {
+exports.parse = function (exp, compiler, data) {
     // unicode and 'constructor' are not allowed for XSS security.
     if (unicodeRE.test(exp) || constructorRE.test(exp)) {
         utils.warn('Unsafe expression: ' + exp)
@@ -146,7 +152,7 @@ exports.parse = function (exp, compiler) {
         // keep track of the first char
         var c = path.charAt(0)
         path = path.slice(1)
-        var val = 'this.' + getRel(path, compiler) + path
+        var val = 'this.' + getRel(path, compiler, data) + path
         if (!has[path]) {
             accessors += val + ';'
             has[path] = 1
@@ -160,4 +166,20 @@ exports.parse = function (exp, compiler) {
     }
 
     return makeGetter(body, exp)
+}
+
+/**
+ *  Evaluate an expression in the context of a compiler.
+ *  Accepts additional data.
+ */
+exports.eval = function (exp, compiler, data) {
+    var getter = exports.parse(exp, compiler, data), res
+    if (getter) {
+        // hack: temporarily attach the additional data so
+        // it can be accessed in the getter
+        compiler.vm.$temp = data
+        res = getter.call(compiler.vm)
+        delete compiler.vm.$temp
+    }
+    return res
 }

+ 32 - 4
src/utils.js

@@ -1,5 +1,4 @@
 var config    = require('./config'),
-    attrs     = config.attrs,
     toString  = ({}).toString,
     win       = window,
     console   = win.console,
@@ -9,6 +8,33 @@ var config    = require('./config'),
 
 var utils = module.exports = {
 
+    /**
+     *  get a value from an object keypath
+     */
+    get: function (obj, key) {
+        var path = key.split('.'),
+            d = -1, l = path.length
+        while (++d < l && obj !== undefined) {
+            obj = obj[path[d]]
+        }
+        return obj
+    },
+
+    /**
+     *  set a value to an object keypath
+     */
+    set: function (obj, key, val) {
+        var path = key.split('.'),
+            d = -1, l = path.length - 1
+        while (++d < l) {
+            if (obj[path[d]] === undefined) {
+                obj[path[d]] = {}
+            }
+            obj = obj[path[d]]
+        }
+        obj[path[d]] = val
+    },
+
     /**
      *  Create a prototype-less object
      *  which is a better hash/map
@@ -20,10 +46,12 @@ var utils = module.exports = {
     /**
      *  get an attribute and remove it.
      */
-    attr: function (el, type) {
-        var attr = attrs[type],
+    attr: function (el, type, preserve) {
+        var attr = config.prefix + '-' + type,
             val = el.getAttribute(attr)
-        if (val !== null) el.removeAttribute(attr)
+        if (!preserve && val !== null) {
+            el.removeAttribute(attr)
+        }
         return val
     },
 

+ 9 - 6
src/viewmodel.js

@@ -24,17 +24,20 @@ function ViewModel (options) {
 // so it can be stringified/looped through as raw data
 var VMProto = ViewModel.prototype
 
+/**
+ *  Convenience function to get a value from
+ *  a keypath
+ */
+def(VMProto, '$get', function (key, value) {
+    return utils.get(this, key, value)
+})
+
 /**
  *  Convenience function to set an actual nested value
  *  from a flat key string. Used in directives.
  */
 def(VMProto, '$set', function (key, value) {
-    var path = key.split('.'),
-        obj = this
-    for (var d = 0, l = path.length - 1; d < l; d++) {
-        obj = obj[path[d]]
-    }
-    obj[path[d]] = value
+    utils.set(this, key, value)
 })
 
 /**

+ 17 - 2
test/functional/fixtures/component.html

@@ -19,7 +19,13 @@
         {{childHi}} {{childName}}
     </div>
 
-    <div id="component-with-sync" v-component="sync" v-with="childHi:hi, childName:user.name">
+    <div id="component-with-sync" v-component="sync" v-with="childHi:hi, childName:user.name"></div>
+
+    <!-- conditional component -->
+    <div id="conditional" v-component="{{ok ? 'my-element' : 'nope'}}"></div>
+
+    <!-- conditional component with v-repeat! -->
+    <div class="repeat-conditional {{type}}" v-repeat="items" v-component="{{type}}"></div>
 </div>
 
 <script src="../../../dist/vue.js"></script>
@@ -46,13 +52,22 @@
         }
     })
 
+    Vue.component('nope', {
+        template: 'NOPE'
+    })
+
     var app = new Vue({
         el: '#test',
         data: {
+            ok: true,
             hi: '123',
             user: {
                 name: 'Jack'
-            }
+            },
+            items: [
+                { type: 'my-element' },
+                { type: 'nope' }
+            ]
         }
     })
 </script>

+ 5 - 1
test/functional/specs/component.js

@@ -1,4 +1,4 @@
-casper.test.begin('Components', 7, function (test) {
+casper.test.begin('Components', 11, function (test) {
     
     casper
     .start('./fixtures/component.html')
@@ -11,6 +11,10 @@ casper.test.begin('Components', 7, function (test) {
         test.assertSelectorHasText('#element', expected)
         test.assertSelectorHasText('#with-sync', expected)
         test.assertSelectorHasText('#component-with-sync', expected)
+        test.assertSelectorHasText('#conditional', expected)
+        test.assertElementCount('.repeat-conditional', 2)
+        test.assertSelectorHasText('.repeat-conditional.my-element', expected)
+        test.assertSelectorHasText('.repeat-conditional.nope', 'NOPE')
     })
     .run(function () {
         test.done()

+ 20 - 9
test/unit/specs/compiler.js

@@ -1,17 +1,28 @@
+// Only methods with no side effects are tested here
+
 describe('Compiler', function () {
     
     describe('.eval()', function () {
 
-        it('should eval correct value', function () {
-            var v = new Vue({
-                data: {
-                    b: 1,
-                    c: {
-                        d: 2
-                    }
+        var v = new Vue({
+            data: {
+                b: 1,
+                c: {
+                    d: 2
                 }
-            })
-            assert.strictEqual(v.$compiler.eval('a {{b}} {{b + c.d}} c'), 'a 1 3 c')
+            }
+        })
+
+        it('should eval correct value', function () {
+            var res = v.$compiler.eval('a {{b}} {{b + c.d}} c')
+            assert.strictEqual(res, 'a 1 3 c')
+        })
+
+        it('should accept additional data', function () {
+            var res = v.$compiler.eval('{{c.d}}', { c: { d: 3 } })
+            assert.strictEqual(res, '3')
+            res = v.$compiler.eval('{{c.d === 3 ? "a" : "b"}}', { c: { d: 3 } })
+            assert.strictEqual(res, 'a')
         })
 
     })

+ 30 - 0
test/unit/specs/utils.js

@@ -9,6 +9,36 @@ describe('Utils', function () {
         // testing require fail
         // for code coverage
     }
+
+    describe('get', function () {
+        
+        it('should get value', function () {
+            var obj = { a: { b: { c: 123 }}}
+            assert.strictEqual(utils.get(obj, 'a.b.c'), 123)
+        })
+
+        it('should return undefined if path does not exist', function () {
+            var obj = { a: {}}
+            assert.strictEqual(utils.get(obj, 'a.b.c'), undefined)
+        })
+
+    })
+
+    describe('set', function () {
+        
+        it('should set value', function () {
+            var obj = { a: { b: { c: 0 }}}
+            utils.set(obj, 'a.b.c', 123)
+            assert.strictEqual(obj.a.b.c, 123)
+        })
+
+        it('should set even if path does not exist', function () {
+            var obj = {}
+            utils.set(obj, 'a.b.c', 123)
+            assert.strictEqual(obj.a.b.c, 123)
+        })
+
+    })
     
     describe('hash', function () {
 

+ 8 - 1
test/unit/specs/viewmodel.js

@@ -17,9 +17,16 @@ describe('ViewModel', function () {
             }
         })
 
+    describe('.$get()', function () {
+        it('should set correct value', function () {
+            var v = vm.$get('a.b.c')
+            assert.strictEqual(v, 12345)
+        })
+    })
+
     describe('.$set()', function () {
-        vm.$set('a.b.c', 54321)
         it('should set correct value', function () {
+            vm.$set('a.b.c', 54321)
             assert.strictEqual(data.b.c, 54321)
         })
     })