Procházet zdrojové kódy

prop: further differentiate one-time, oneway-up and oneway-down

Evan You před 11 roky
rodič
revize
dcf5fcdfec

+ 44 - 21
src/compiler/compile.js

@@ -399,17 +399,21 @@ function makeChildLinkFn (linkFns) {
  * @return {Function} propsLinkFn
  */
 
-// regex to test if a path is "settable"
-// if not the prop binding is automatically one-way.
+var dataAttrRE = /^data-/
 var settablePathRE = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*|\[[^\[\]]+\])*$/
 var literalValueRE = /^true|false|\d+$/
+var identRE = require('../parsers/path').identRE
 
 function compileProps (el, attrs, propNames) {
   var props = []
   var i = propNames.length
-  var name, value, prop
+  var name, value, path, prop, settable, single
   while (i--) {
     name = propNames[i]
+    // props could contain dashes, which will be
+    // interpreted as minus calculations by the parser
+    // so we need to camelize the path here
+    path = _.camelize(name.replace(dataAttrRE, ''))
     if (/[A-Z]/.test(name)) {
       _.warn(
         'You seem to be using camelCase for a component prop, ' +
@@ -419,26 +423,48 @@ function compileProps (el, attrs, propNames) {
         'http://vuejs.org/api/options.html#props'
       )
     }
+    if (!identRE.test(path)) {
+      _.warn(
+        'Invalid prop key: "' + name + '". Prop keys ' +
+        'must be valid identifiers.'
+      )
+    }
     value = attrs[name]
     /* jshint eqeqeq:false */
     if (value != null) {
       prop = {
         name: name,
-        raw: value
+        raw: value,
+        path: path
       }
       var tokens = textParser.parse(value)
       if (tokens) {
         if (el && el.nodeType === 1) {
           el.removeAttribute(name)
         }
+        // important so that this doesn't get compiled
+        // again as a normal attribute binding
         attrs[name] = null
         prop.dynamic = true
-        prop.value = textParser.tokensToExp(tokens)
+        prop.parentPath = textParser.tokensToExp(tokens)
+        // check prop binding type.
+        single = tokens.length === 1
+        settable =
+          settablePathRE.test(prop.parentPath) &&
+          !literalValueRE.test(prop.parentPath)
+        // one time: {{* prop}}
         prop.oneTime =
-          tokens.length > 1 ||
-          tokens[0].oneTime ||
-          !settablePathRE.test(prop.value) ||
-          literalValueRE.test(prop.value)
+          !single ||
+          !settable ||
+          tokens[0].oneTime
+        // one way down: {{> prop}}
+        prop.oneWayDown =
+          single &&
+          tokens[0].oneWay === 62 // >
+        // one way up: {{< prop}}
+        prop.oneWayUp =
+          tokens[0].oneWay === 60 && // <
+          settable
       }
       props.push(prop)
     }
@@ -453,25 +479,22 @@ function compileProps (el, attrs, propNames) {
  * @return {Function} propsLinkFn
  */
 
-var dataAttrRE = /^data-/
-
 function makePropsLinkFn (props) {
   return function propsLinkFn (vm, el) {
     var i = props.length
     var prop, path
     while (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(prop.name.replace(dataAttrRE, ''))
+      path = prop.path
       if (prop.dynamic) {
         if (vm.$parent) {
-          vm._bindDir('prop', el, {
-            arg: path,
-            expression: prop.value,
-            oneWay: prop.oneTime
-          }, propDef)
+          if (prop.onetime) {
+            // one time binding
+            vm.$set(path, vm.$parent.$get(prop.parentPath))
+          } else {
+            // dynamic binding
+            vm._bindDir('prop', el, prop, propDef)
+          }
         } else {
           _.warn(
             'Cannot bind dynamic prop on a root instance' +
@@ -480,7 +503,7 @@ function makePropsLinkFn (props) {
           )
         }
       } else {
-        // just set once
+        // literal, just set once
         vm.$set(path, _.toNumber(prop.raw))
       }
     }

+ 28 - 28
src/directives/prop.js

@@ -1,6 +1,5 @@
 var _ = require('../util')
 var Watcher = require('../watcher')
-var identRE = require('../parsers/path').identRE
 
 module.exports = {
 
@@ -8,15 +7,10 @@ module.exports = {
 
     var child = this.vm
     var parent = child.$parent
-    var childKey = this.arg
-    var parentKey = this.expression
-
-    if (!identRE.test(childKey)) {
-      _.warn(
-        'Invalid prop key: "' + childKey + '". Prop keys ' +
-        'must be valid identifiers.'
-      )
-    }
+    // passed in from compiler directly
+    var prop = this._descriptor
+    var childKey = prop.path
+    var parentKey = prop.parentPath
 
     // simple lock to avoid circular updates.
     // without this it would stabilize too, but this makes
@@ -30,26 +24,28 @@ module.exports = {
       locked = false
     }
 
-    this.parentWatcher = new Watcher(
-      parent,
-      parentKey,
-      function (val) {
-        if (!locked) {
-          lock()
-          // all props have been initialized already
-          child[childKey] = val
+    if (!prop.oneWayUp) {
+      this.parentWatcher = new Watcher(
+        parent,
+        parentKey,
+        function (val) {
+          if (!locked) {
+            lock()
+            // all props have been initialized already
+            child[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)
+      )
+      
+      // 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.
-    if (!this._descriptor.oneWay) {
+    if (!prop.oneWayDown) {
       this.childWatcher = new Watcher(
         child,
         childKey,
@@ -60,6 +56,11 @@ module.exports = {
           }
         }
       )
+
+      // set initial value for one-way up binding
+      if (prop.oneWayUp) {
+        parent.$set(parentKey, this.childWatcher.value)
+      }
     }
   },
 
@@ -71,5 +72,4 @@ module.exports = {
       this.childWatcher.teardown()
     }
   }
-
-}
+}

+ 7 - 5
src/parsers/text.js

@@ -70,7 +70,7 @@ exports.parse = function (text) {
   }
   var tokens = []
   var lastIndex = tagRE.lastIndex = 0
-  var match, index, value, first, oneTime
+  var match, index, value, first, oneTime, oneWay
   /* jshint boss:true */
   while (match = tagRE.exec(text)) {
     index = match.index
@@ -82,15 +82,17 @@ exports.parse = function (text) {
     }
     // tag token
     first = match[1].charCodeAt(0)
-    oneTime = first === 0x2A // *
-    value = oneTime
+    oneTime = first === 42    // *
+    oneWay = first === 62 || first === 60 // > or <
+    value = oneTime || oneWay
       ? match[1].slice(1)
       : match[1]
     tokens.push({
       tag: true,
       value: value.trim(),
       html: htmlRE.test(match[0]),
-      oneTime: oneTime
+      oneTime: oneTime,
+      oneWay: oneWay ? first : 0
     })
     lastIndex = index + match[0].length
   }
@@ -169,4 +171,4 @@ function inlineFilters (exp, single) {
         ',false)'        // write?
     }
   }
-}
+}

+ 41 - 19
test/unit/specs/compiler/compile_spec.js

@@ -153,7 +153,9 @@ if (_.inBrowser) {
           'data-some-attr',
           'some-other-attr',
           'multiple-attrs',
-          'oneway',
+          'onetime',
+          'oneway-up',
+          'oneway-down',
           'with-filter',
           'camelCase',
           'boolean-literal'
@@ -164,49 +166,69 @@ if (_.inBrowser) {
       el.setAttribute('data-some-attr', '{{a}}')
       el.setAttribute('some-other-attr', '2')
       el.setAttribute('multiple-attrs', 'a {{b}} c')
-      el.setAttribute('oneway', '{{*a}}')
+      el.setAttribute('onetime', '{{*a}}')
+      el.setAttribute('oneway-up', '{{<a}}')
+      el.setAttribute('oneway-down', '{{>a}}')
       el.setAttribute('with-filter', '{{a | filter}}')
       el.setAttribute('boolean-literal', '{{true}}')
       transclude(el, options)
       compiler.compileAndLinkRoot(vm, el, options)
       // should skip literals and one-time bindings
-      expect(vm._bindDir.calls.count()).toBe(5)
+      expect(vm._bindDir.calls.count()).toBe(7)
       // data-some-attr
       var args = vm._bindDir.calls.argsFor(0)
       expect(args[0]).toBe('prop')
       expect(args[1]).toBe(null)
-      expect(args[2].arg).toBe('someAttr')
-      expect(args[2].expression).toBe('a')
+      expect(args[2].path).toBe('someAttr')
+      expect(args[2].parentPath).toBe('a')
       expect(args[3]).toBe(def)
       // multiple-attrs
       args = vm._bindDir.calls.argsFor(1)
       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[2].path).toBe('multipleAttrs')
+      expect(args[2].parentPath).toBe('"a "+(b)+" c"')
       expect(args[3]).toBe(def)
-      // oneway
+      // one time
       args = vm._bindDir.calls.argsFor(2)
       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[2].path).toBe('onetime')
+      expect(args[2].oneTime).toBe(true)
+      expect(args[2].parentPath).toBe('a')
       expect(args[3]).toBe(def)
-      // with-filter
+      // one way up
       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._applyFilters(a,null,[{"name":"filter"}],false)')
+      expect(args[2].path).toBe('onewayUp')
+      expect(args[2].oneWayUp).toBe(true)
+      expect(args[2].oneWayDown).toBe(false)
+      expect(args[2].parentPath).toBe('a')
       expect(args[3]).toBe(def)
-      // boolean-literal
+      // one way down
       args = vm._bindDir.calls.argsFor(4)
       expect(args[0]).toBe('prop')
       expect(args[1]).toBe(null)
-      expect(args[2].arg).toBe('booleanLiteral')
-      expect(args[2].expression).toBe('true')
-      expect(args[2].oneWay).toBe(true)
+      expect(args[2].path).toBe('onewayDown')
+      expect(args[2].oneWayUp).toBe(false)
+      expect(args[2].oneWayDown).toBe(true)
+      expect(args[2].parentPath).toBe('a')
+      expect(args[3]).toBe(def)
+      // with-filter
+      args = vm._bindDir.calls.argsFor(5)
+      expect(args[0]).toBe('prop')
+      expect(args[1]).toBe(null)
+      expect(args[2].path).toBe('withFilter')
+      expect(args[2].parentPath).toBe('this._applyFilters(a,null,[{"name":"filter"}],false)')
+      expect(args[3]).toBe(def)
+      // boolean-literal
+      args = vm._bindDir.calls.argsFor(6)
+      expect(args[0]).toBe('prop')
+      expect(args[1]).toBe(null)
+      expect(args[2].path).toBe('booleanLiteral')
+      expect(args[2].parentPath).toBe('true')
+      expect(args[2].oneTime).toBe(true)
       // camelCase should've warn
       expect(hasWarned(_, 'using camelCase')).toBe(true)
       // literal and one time should've called vm.$set
@@ -408,4 +430,4 @@ if (_.inBrowser) {
     })
 
   })
-}
+}