Evan You 11 лет назад
Родитель
Сommit
ee2b99efd7

+ 1 - 1
component.json

@@ -38,13 +38,13 @@
     "src/directives/model/radio.js",
     "src/directives/model/select.js",
     "src/directives/on.js",
+    "src/directives/prop.js",
     "src/directives/ref.js",
     "src/directives/repeat.js",
     "src/directives/show.js",
     "src/directives/style.js",
     "src/directives/text.js",
     "src/directives/transition.js",
-    "src/directives/with.js",
     "src/filters/array-filters.js",
     "src/filters/index.js",
     "src/instance/compile.js",

+ 1 - 1
examples/svg/index.html

@@ -24,7 +24,7 @@
     <div id="demo">
       <!-- Use the component -->
       <svg width="200" height="200">
-        <g v-component="polygraph" v-with="stats:stats"></g>
+        <g v-component="polygraph" stats="{{stats}}"></g>
       </svg>
       <!-- controls -->
       <div v-repeat="stats">

+ 1 - 0
examples/svg/svg.js

@@ -10,6 +10,7 @@ var stats = [
 
 // A resusable polygon graph component
 Vue.component('polygraph', {
+  props: ['stats'],
   template: '#polygraph-template',
   computed: {
     // a computed property for the polygon's points

+ 1 - 1
examples/tree/index.html

@@ -46,7 +46,7 @@
     <ul id="demo">
       <li class="item"
         v-component="item"
-        v-with="model: treeData">
+        model="{{treeData}}">
       </li>
     </ul>
 

+ 1 - 0
examples/tree/tree.js

@@ -30,6 +30,7 @@ var data = {
 
 // define the item component
 Vue.component('item', {
+  props: ['model'],
   template: '#item-template',
   data: function () {
     return {

+ 34 - 52
src/compiler/compile.js

@@ -4,6 +4,10 @@ var textParser = require('../parsers/text')
 var dirParser = require('../parsers/directive')
 var templateParser = require('../parsers/template')
 
+// internal directives
+var propDef = require('../directives/prop')
+// var componentDef = require('../directives/component')
+
 module.exports = compile
 
 /**
@@ -108,12 +112,11 @@ function teardownDirs (vm, dirs, destroying) {
 
 /**
  * Compile the root element of a component. There are
- * 4 types of things to process here:
+ * 3 types of things to process here:
  * 
  * 1. props on parent container (child scope)
- * 2. v-with on parent container (child scope)
- * 3. other attrs on parent container (parent scope)
- * 4. attrs on the component template root node, if
+ * 2. other attrs on parent container (parent scope)
+ * 3. attrs on the component template root node, if
  *    replace:true (child scope)
  *
  * Also, if this is a block instance, we only need to
@@ -129,37 +132,25 @@ function compileRoot (el, options) {
   var containerAttrs = options._containerAttrs
   var replacerAttrs = options._replacerAttrs
   var props = options.props
-  var propsLinkFn, withLinkFn, parentLinkFn, replacerLinkFn
+  var propsLinkFn, parentLinkFn, replacerLinkFn
   // 1. props
   propsLinkFn = props
-    ? compileProps(el, containerAttrs, props, options)
+    ? compileProps(el, containerAttrs, props)
     : null
-  // 2. v-with
-  var withName = config.prefix + 'with'
-  var withVal = containerAttrs && containerAttrs[withName]
-  if (withVal) {
-    containerAttrs[withName] = null
-    withLinkFn = makeNodeLinkFn([{
-      name: 'with',
-      descriptors: dirParser.parse(withVal),
-      def: options.directives['with']
-    }])
-  }
   if (!isBlock) {
-    // 3. container attributes
+    // 2. container attributes
     if (containerAttrs) {
       parentLinkFn = compileDirectives(containerAttrs, options)
     }
     if (replacerAttrs) {
-      // 4. replacer attributes
+      // 3. replacer attributes
       replacerLinkFn = compileDirectives(replacerAttrs, options)
     }
   }
   return function rootLinkFn (vm, el, host) {
-    // explicitly passing null to props and v-with
+    // explicitly passing null to props
     // linkers because they don't need a real element
     if (propsLinkFn) propsLinkFn(vm, null)
-    if (withLinkFn) withLinkFn(vm, null)
     if (parentLinkFn) parentLinkFn(vm.$parent, el, host)
     if (replacerLinkFn) replacerLinkFn(vm, el, host)
   }
@@ -384,14 +375,13 @@ function makeChildLinkFn (linkFns) {
  * @param {Element|DocumentFragment} el
  * @param {Object} attrs
  * @param {Array} propNames
- * @param {Object} options
  * @return {Function} propsLinkFn
  */
 
-function compileProps (el, attrs, propNames, options) {
+function compileProps (el, attrs, propNames) {
   var props = []
   var i = propNames.length
-  var name, value, param
+  var name, value, prop
   while (i--) {
     name = propNames[i]
     if (/[A-Z]/.test(name)) {
@@ -404,8 +394,9 @@ function compileProps (el, attrs, propNames, options) {
       )
     }
     value = attrs[name]
-    if (value !== null) {
-      param = {
+    /* jshint eqeqeq:false */
+    if (value != null) {
+      prop = {
         name: name,
         value: value
       }
@@ -415,53 +406,44 @@ function compileProps (el, attrs, propNames, options) {
           el.removeAttribute(name)
         }
         attrs[name] = null
-        param.dynamic = true
-        param.value = textParser.tokensToExp(tokens)
-        param.oneTime = tokens.length === 1 && tokens[0].oneTime
+        prop.dynamic = true
+        prop.value = textParser.tokensToExp(tokens)
+        prop.oneTime = tokens.length === 1 && tokens[0].oneTime
       }
-      props.push(param)
+      props.push(prop)
     }
   }
-  return makeParamsLinkFn(props, options)
+  return makePropsLinkFn(props)
 }
 
 /**
- * Build a function that applies param attributes to a vm.
+ * Build a function that applies props to a vm.
  *
  * @param {Array} props
- * @param {Object} options
  * @return {Function} propsLinkFn
  */
 
 var dataAttrRE = /^data-/
 
-function makeParamsLinkFn (props, options) {
-  var def = options.directives['with']
+function makePropsLinkFn (props) {
   return function propsLinkFn (vm, el) {
     var i = props.length
-    var param, path
+    var prop, path
     while (i--) {
-      param = props[i]
+      prop = props[i]
       // props could contain dashes, which will be
       // interpreted as minus calculations by the parser
       // so we need to wrap the path here
-      path = _.camelize(param.name.replace(dataAttrRE, ''))
-      if (param.dynamic) {
-        if (param.oneTime) {
-          vm.$set(path, vm.$parent.$get(param.value))
-        } else {
-          // dynamic param attribtues are bound as v-with.
-          // we can directly duck the descriptor here beacuse
-          // param attributes cannot use expressions or
-          // filters.
-          vm._bindDir('with', el, {
-            arg: path,
-            expression: param.value
-          }, def)
-        }
+      path = _.camelize(prop.name.replace(dataAttrRE, ''))
+      if (prop.dynamic) {
+        vm._bindDir('prop', el, {
+          arg: path,
+          expression: prop.value,
+          oneWay: prop.oneTime
+        }, propDef)
       } else {
         // just set once
-        vm.$set(path, param.value)
+        vm.$set(path, prop.value)
       }
     }
   }

+ 1 - 0
src/directive.js

@@ -33,6 +33,7 @@ function Directive (name, el, vm, descriptor, def, host) {
   this.arg = descriptor.arg
   this.filters = _.resolveFilters(vm, descriptor.filters)
   // private
+  this._descriptor = descriptor
   this._host = host
   this._locked = false
   this._bound = false

+ 1 - 0
src/directives/component.js

@@ -3,6 +3,7 @@ var templateParser = require('../parsers/template')
 
 module.exports = {
 
+  _internal: true,
   isLiteral: true,
 
   /**

+ 7 - 4
src/directives/index.js

@@ -14,11 +14,14 @@ exports.transition = require('./transition')
 exports.on         = require('./on')
 exports.model      = require('./model')
 
-// child vm directives
-exports.component  = require('./component')
+// logic control directives
 exports.repeat     = require('./repeat')
 exports['if']      = require('./if')
 
 // child vm communication directives
-exports['with']    = require('./with')
-exports.events     = require('./events')
+exports.events     = require('./events')
+
+// internal directives that should not be used directly
+// but we still want to expose them for advanced usage.
+exports.component = require('./component')
+exports._prop      = require('./prop')

+ 72 - 0
src/directives/prop.js

@@ -0,0 +1,72 @@
+var _ = require('../util')
+var Watcher = require('../watcher')
+var expParser = require('../parsers/expression')
+
+module.exports = {
+
+  _internal: true,
+
+  bind: function () {
+
+    var child = this.vm
+    var parent = child.$parent
+    var childKey = this.arg
+    var parentKey = this.expression
+
+    // simple lock to avoid circular updates.
+    // without this it would stabilize too, but this makes
+    // sure it doesn't cause other watchers to re-evaluate.
+    var locked = false
+    var lock = function () {
+      locked = true
+      _.nextTick(unlock)
+    }
+    var unlock = function () {
+      locked = false
+    }
+
+    this.parentWatcher = new Watcher(
+      parent,
+      parentKey,
+      function (val) {
+        if (!locked) {
+          lock()
+          child.$set(childKey, val)
+        }
+      }
+    )
+    
+    // set the child initial value first, before setting
+    // up the child watcher to avoid triggering it
+    // immediately.
+    child.$set(childKey, this.parentWatcher.value)
+
+    // only setup two-way binding if this is not a one-way
+    // binding, and the parentKey is a "settable" simple path.
+    if (
+      !this._descriptor.oneWay &&
+      expParser.isSimplePath(parentKey)
+    ) {
+      this.childWatcher = new Watcher(
+        child,
+        childKey,
+        function (val) {
+          if (!locked) {
+            lock()
+            parent.$set(parentKey, val)
+          }
+        }
+      )
+    }
+  },
+
+  unbind: function () {
+    if (this.parentWatcher) {
+      this.parentWatcher.teardown()
+    }
+    if (this.childWatcher) {
+      this.childWatcher.teardown()
+    }
+  }
+
+}

+ 0 - 90
src/directives/with.js

@@ -1,90 +0,0 @@
-var _ = require('../util')
-var Watcher = require('../watcher')
-var expParser = require('../parsers/expression')
-var literalRE = /^(true|false|\s?('[^']*'|"[^"]")\s?)$/
-
-module.exports = {
-
-  priority: 900,
-
-  bind: function () {
-
-    var child = this.vm
-    var parent = child.$parent
-    var childKey = this.arg || '$data'
-    var parentKey = this.expression
-
-    if (this.el && this.el !== child.$el) {
-      _.warn(
-        'v-with can only be used on instance root elements.'
-      )
-    } else if (!parent) {
-      _.warn(
-        'v-with must be used on an instance with a parent.'
-      )
-    } else if (literalRE.test(parentKey)) {
-      // no need to setup watchers for literal bindings
-      if (!this.arg) {
-        _.warn(
-          'v-with cannot bind literal value as $data: ' +
-          parentKey
-        )
-      } else {
-        var value = expParser.parse(parentKey).get()
-        child.$set(childKey, value)
-      }
-    } else {
-
-      // simple lock to avoid circular updates.
-      // without this it would stabilize too, but this makes
-      // sure it doesn't cause other watchers to re-evaluate.
-      var locked = false
-      var lock = function () {
-        locked = true
-        _.nextTick(unlock)
-      }
-      var unlock = function () {
-        locked = false
-      }
-
-      this.parentWatcher = new Watcher(
-        parent,
-        parentKey,
-        function (val) {
-          if (!locked) {
-            lock()
-            child.$set(childKey, val)
-          }
-        }
-      )
-      
-      // set the child initial value first, before setting
-      // up the child watcher to avoid triggering it
-      // immediately.
-      child.$set(childKey, this.parentWatcher.value)
-
-      // only setup two-way binding if the parentKey is
-      // a "settable" simple path.
-      if (expParser.isSimplePath(parentKey)) {
-        this.childWatcher = new Watcher(
-          child,
-          childKey,
-          function (val) {
-            if (!locked) {
-              lock()
-              parent.$set(parentKey, val)
-            }
-          }
-        )
-      }
-    }
-  },
-
-  unbind: function () {
-    if (this.parentWatcher) {
-      this.parentWatcher.teardown()
-      this.childWatcher.teardown()
-    }
-  }
-
-}

+ 4 - 4
src/instance/scope.js

@@ -26,11 +26,11 @@ exports._initData = function () {
   var data = this._data
   var i, key
   // make sure all props properties are observed
-  var params = this.$options.props
-  if (params) {
-    i = params.length
+  var props = this.$options.props
+  if (props) {
+    i = props.length
     while (i--) {
-      key = _.camelize(params[i])
+      key = _.camelize(props[i])
       if (!(key in data)) {
         data[key] = null
       }

+ 18 - 11
test/unit/specs/compiler/compile_spec.js

@@ -147,7 +147,7 @@ if (_.inBrowser) {
       expect(el.firstChild.getAttribute('b')).toBe('B')
     })
 
-    it('param attributes', function () {
+    it('props', function () {
       var options = merge(Vue.options, {
         _asComponent: true,
         props: [
@@ -155,49 +155,56 @@ if (_.inBrowser) {
           'data-some-attr',
           'some-other-attr',
           'multiple-attrs',
-          'onetime',
+          'oneway',
           'with-filter',
           'camelCase'
         ]
       })
-      var def = Vue.options.directives['with']
+      var def = Vue.options.directives._prop
       el.setAttribute('a', '1')
       el.setAttribute('data-some-attr', '{{a}}')
       el.setAttribute('some-other-attr', '2')
       el.setAttribute('multiple-attrs', 'a {{b}} c')
-      el.setAttribute('onetime', '{{*a}}')
+      el.setAttribute('oneway', '{{*a}}')
       el.setAttribute('with-filter', '{{a | filter}}')
       transclude(el, options)
       var linker = compile(el, options)
       linker(vm, el)
       // should skip literals and one-time bindings
-      expect(vm._bindDir.calls.count()).toBe(3)
+      expect(vm._bindDir.calls.count()).toBe(4)
       // data-some-attr
       var args = vm._bindDir.calls.argsFor(0)
-      expect(args[0]).toBe('with')
+      expect(args[0]).toBe('prop')
       expect(args[1]).toBe(null)
       expect(args[2].arg).toBe('someAttr')
       expect(args[2].expression).toBe('a')
       expect(args[3]).toBe(def)
       // multiple-attrs
       args = vm._bindDir.calls.argsFor(1)
-      expect(args[0]).toBe('with')
+      expect(args[0]).toBe('prop')
       expect(args[1]).toBe(null)
       expect(args[2].arg).toBe('multipleAttrs')
       expect(args[2].expression).toBe('"a "+(b)+" c"')
       expect(args[3]).toBe(def)
-      // with-filter
+      // oneway
       args = vm._bindDir.calls.argsFor(2)
-      expect(args[0]).toBe('with')
+      expect(args[0]).toBe('prop')
+      expect(args[1]).toBe(null)
+      expect(args[2].arg).toBe('oneway')
+      expect(args[2].oneWay).toBe(true)
+      expect(args[2].expression).toBe('a')
+      expect(args[3]).toBe(def)
+      // with-filter
+      args = vm._bindDir.calls.argsFor(3)
+      expect(args[0]).toBe('prop')
       expect(args[1]).toBe(null)
       expect(args[2].arg).toBe('withFilter')
       expect(args[2].expression).toBe('this._applyFilter("filter",[a])')
-      expect(args[3]).toBe(def) 
+      expect(args[3]).toBe(def)
       // camelCase should've warn
       expect(_.warn.calls.count()).toBe(1)
       // literal and one time should've called vm.$set
       expect(vm.$set).toHaveBeenCalledWith('a', '1')
-      expect(vm.$set).toHaveBeenCalledWith('onetime', 'from parent: a')
       expect(vm.$set).toHaveBeenCalledWith('someOtherAttr', '2')
     })
 

+ 108 - 0
test/unit/specs/directives/prop_spec.js

@@ -0,0 +1,108 @@
+var _ = require('../../../../src/util')
+var Vue = require('../../../../src/vue')
+
+if (_.inBrowser) {
+  describe('prop', function () {
+
+    var el
+    beforeEach(function () {
+      el = document.createElement('div')
+      spyOn(_, 'warn')
+    })
+
+    it('should work', function (done) {
+      var vm = new Vue({
+        el: el,
+        data: {
+          b: 'B',
+          test: {
+            a: 'A'
+          }
+        },
+        template: '<div v-component="test" testt="{{test}}" bb="{{b}}" v-ref="child"></div>',
+        components: {
+          test: {
+            props: ['testt', 'bb'],
+            template: '{{testt.a}} {{bb}}'
+          }
+        }
+      })
+      expect(el.firstChild.textContent).toBe('A B')
+      vm.test.a = 'AA'
+      vm.b = 'BB'
+      _.nextTick(function () {
+        expect(el.firstChild.textContent).toBe('AA BB')
+        vm.test = { a: 'AAA' }
+        _.nextTick(function () {
+          expect(el.firstChild.textContent).toBe('AAA BB')
+          vm.$data = {
+            b: 'BBB',
+            test: {
+              a: 'AAAA'
+            }
+          }
+          _.nextTick(function () {
+            expect(el.firstChild.textContent).toBe('AAAA BBB')
+            // test two-way
+            vm.$.child.bb = 'B'
+            vm.$.child.testt = { a: 'A' }
+            _.nextTick(function () {
+              expect(el.firstChild.textContent).toBe('A B')
+              expect(vm.test.a).toBe('A')
+              expect(vm.test).toBe(vm.$.child.testt)
+              expect(vm.b).toBe('B')
+              done()
+            })
+          })
+        })
+      })
+    })
+
+    it('teardown', function (done) {
+      var vm = new Vue({
+        el: el,
+        data: {
+          b: 'B'
+        },
+        template: '<div v-component="test" bb="{{b}}"></div>',
+        components: {
+          test: {
+            props: ['bb'],
+            template: '{{bb}}'
+          }
+        }
+      })
+      expect(el.firstChild.textContent).toBe('B')
+      vm.b = 'BB'
+      _.nextTick(function () {
+        expect(el.firstChild.textContent).toBe('BB')
+        vm._children[0]._directives[0].unbind()
+        vm.b = 'BBB'
+        _.nextTick(function () {
+          expect(el.firstChild.textContent).toBe('BB')
+          done()
+        })
+      })
+    })
+
+    it('block instance with replace:true', function () {
+      var vm = new Vue({
+        el: el,
+        template: '<div v-component="test" b="{{a}}" c="{{d}}"></div>',
+        data: {
+          a: 'AAA',
+          d: 'DDD'
+        },
+        components: {
+          test: {
+            props: ['b', 'c'],
+            template: '<p>{{b}}</p><p>{{c}}</p>',
+            replace: true
+          }
+        }
+      })
+      expect(el.innerHTML).toBe('<!--v-start--><p>AAA</p><p>DDD</p><!--v-end--><!--v-component-->')
+    })
+
+  })
+}

+ 0 - 186
test/unit/specs/directives/with_spec.js

@@ -1,186 +0,0 @@
-var _ = require('../../../../src/util')
-var Vue = require('../../../../src/vue')
-
-if (_.inBrowser) {
-  describe('v-with', function () {
-
-    var el
-    beforeEach(function () {
-      el = document.createElement('div')
-      spyOn(_, 'warn')
-    })
-
-    it('no arg', function (done) {
-      var vm = new Vue({
-        el: el,
-        data: {
-          test: {
-            a: 'A'
-          }
-        },
-        template: '<div v-component="test" v-with="test"></div>',
-        components: {
-          test: {
-            template: '{{a}}'
-          }
-        }
-      })
-      expect(el.firstChild.textContent).toBe('A')
-      // swap nested prop
-      vm.test.a = 'B'
-      _.nextTick(function () {
-        expect(el.firstChild.textContent).toBe('B')
-        // swap passed down prop
-        vm.test = { a: 'C' }
-        _.nextTick(function () {
-          expect(el.firstChild.textContent).toBe('C')
-          // swap root $data
-          vm.$data = { test: { a: 'D' }}
-          _.nextTick(function () {
-            expect(el.firstChild.textContent).toBe('D')
-            done()
-          })
-        })
-      })
-    })
-
-    it('with arg', function (done) {
-      var vm = new Vue({
-        el: el,
-        data: {
-          b: 'B',
-          test: {
-            a: 'A'
-          }
-        },
-        template: '<div v-component="test" v-with="testt:test,bb:b" v-ref="child"></div>',
-        components: {
-          test: {
-            template: '{{testt.a}} {{bb}}'
-          }
-        }
-      })
-      expect(el.firstChild.textContent).toBe('A B')
-      vm.test.a = 'AA'
-      vm.b = 'BB'
-      _.nextTick(function () {
-        expect(el.firstChild.textContent).toBe('AA BB')
-        vm.test = { a: 'AAA' }
-        _.nextTick(function () {
-          expect(el.firstChild.textContent).toBe('AAA BB')
-          vm.$data = {
-            b: 'BBB',
-            test: {
-              a: 'AAAA'
-            }
-          }
-          _.nextTick(function () {
-            expect(el.firstChild.textContent).toBe('AAAA BBB')
-            // test two-way
-            vm.$.child.bb = 'B'
-            vm.$.child.testt = { a: 'A' }
-            _.nextTick(function () {
-              expect(el.firstChild.textContent).toBe('A B')
-              expect(vm.test.a).toBe('A')
-              expect(vm.test).toBe(vm.$.child.testt)
-              expect(vm.b).toBe('B')
-              done()
-            })
-          })
-        })
-      })
-    })
-
-    it('teardown', function (done) {
-      var vm = new Vue({
-        el: el,
-        data: {
-          b: 'B'
-        },
-        template: '<div v-component="test" v-with="bb:b"></div>',
-        components: {
-          test: {
-            template: '{{bb}}'
-          }
-        }
-      })
-      expect(el.firstChild.textContent).toBe('B')
-      vm.b = 'BB'
-      _.nextTick(function () {
-        expect(el.firstChild.textContent).toBe('BB')
-        vm._children[0]._directives[0].unbind()
-        vm.b = 'BBB'
-        _.nextTick(function () {
-          expect(el.firstChild.textContent).toBe('BB')
-          done()
-        })
-      })
-    })
-
-    it('non-root warning', function () {
-      var vm = new Vue({
-        el: el,
-        template: '<div v-with="test"></div>'
-      })
-      expect(hasWarned(_, 'can only be used on instance root elements')).toBe(true)
-    })
-
-    it('no-parent warning', function () {
-      el.setAttribute('v-with', 'test')
-      var vm = new Vue({
-        el: el
-      })
-      expect(hasWarned(_, 'must be used on an instance with a parent')).toBe(true)
-    })
-
-    it('block instance with replace:true', function () {
-      var vm = new Vue({
-        el: el,
-        template: '<div v-component="test" v-with="b:a" c="{{d}}"></div>',
-        data: {
-          a: 'AAA',
-          d: 'DDD'
-        },
-        components: {
-          test: {
-            props: ['c'],
-            template: '<p>{{b}}</p><p>{{c}}</p>',
-            replace: true
-          }
-        }
-      })
-      expect(el.innerHTML).toBe('<!--v-start--><p>AAA</p><p>DDD</p><!--v-end--><!--v-component-->')
-    })
-
-    it('bind literal values should not trigger setter warning', function (done) {
-      var vm = new Vue({
-        el: el,
-        template: '<div v-component="test" v-with="a:\'test\'"></div>',
-        components: {
-          test: {
-            template: '{{a}}'
-          }
-        }
-      })
-      expect(el.firstChild.innerHTML).toBe('test')
-      vm._children[0].a = 'changed'
-      _.nextTick(function () {
-        expect(el.firstChild.innerHTML).toBe('changed')
-        expect(_.warn).not.toHaveBeenCalled()
-        done()
-      })
-    })
-
-    it('should warn when binding literal value without childKey', function () {
-      var vm = new Vue({
-        el: el,
-        template: '<div v-component="test" v-with="\'test\'"></div>',
-        components: {
-          test: {}
-        }
-      })
-      expect(hasWarned(_, 'cannot bind literal value as $data')).toBe(true)
-    })
-
-  })
-}