Przeglądaj źródła

support using literal props on root instance

Evan You 11 lat temu
rodzic
commit
70045b87b2

+ 38 - 23
src/compiler/compile.js

@@ -32,7 +32,7 @@ module.exports = compile
 
 function compile (el, options, partial, transcluded) {
   // link function for the node itself.
-  var nodeLinkFn = options._asComponent && !partial
+  var nodeLinkFn = !partial
     ? compileRoot(el, options)
     : compileNode(el, options)
   // link function for the childNodes
@@ -118,7 +118,7 @@ function teardownDirs (vm, dirs, destroying) {
 }
 
 /**
- * Compile the root element of a component. There are
+ * Compile the root element of an instance. There are
  * 3 types of things to process here:
  * 
  * 1. props on parent container (child scope)
@@ -135,23 +135,31 @@ function teardownDirs (vm, dirs, destroying) {
  */
 
 function compileRoot (el, options) {
-  var isBlock = el.nodeType === 11 // DocumentFragment
   var containerAttrs = options._containerAttrs
   var replacerAttrs = options._replacerAttrs
   var props = options.props
   var propsLinkFn, parentLinkFn, replacerLinkFn
   // 1. props
-  propsLinkFn = props
+  propsLinkFn = props && containerAttrs
     ? compileProps(el, containerAttrs, props)
     : null
-  if (!isBlock) {
-    // 2. container attributes
-    if (containerAttrs) {
-      parentLinkFn = compileDirectives(containerAttrs, options)
-    }
-    if (replacerAttrs) {
-      // 3. replacer attributes
-      replacerLinkFn = compileDirectives(replacerAttrs, options)
+  // only need to compile other attributes for
+  // non-block instances
+  if (el.nodeType !== 11) {
+    // for components, container and replacer need to be
+    // compiled separately and linked in different scopes.
+    if (options._asComponent) {
+      // 2. container attributes
+      if (containerAttrs) {
+        parentLinkFn = compileDirectives(containerAttrs, options)
+      }
+      if (replacerAttrs) {
+        // 3. replacer attributes
+        replacerLinkFn = compileDirectives(replacerAttrs, options)
+      }
+    } else {
+      // non-component, just compile as a normal element.
+      replacerLinkFn = compileDirectives(el, options)
     }
   }
   return function rootLinkFn (vm, el, host) {
@@ -192,17 +200,16 @@ function compileNode (node, options) {
  */
 
 function compileElement (el, options) {
-  if (checkTransclusion(el)) {
+  var hasAttrs = el.hasAttributes()
+  if (hasAttrs && checkTransclusion(el)) {
     // unwrap textNode
     if (el.hasAttribute('__vue__wrap')) {
       el = el.firstChild
     }
     return compile(el, options._parent.$options, true, true)
   }
-  var linkFn
-  var hasAttrs = el.hasAttributes()
   // check element directives
-  linkFn = checkElementDirectives(el, options)
+  var linkFn = checkElementDirectives(el, options)
   // check terminal direcitves (repeat & if)
   if (!linkFn && hasAttrs) {
     linkFn = checkTerminalDirectives(el, options)
@@ -406,7 +413,7 @@ function compileProps (el, attrs, propNames) {
     if (value != null) {
       prop = {
         name: name,
-        value: value
+        raw: value
       }
       var tokens = textParser.parse(value)
       if (tokens) {
@@ -447,14 +454,22 @@ function makePropsLinkFn (props) {
       // so we need to wrap the path here
       path = _.camelize(prop.name.replace(dataAttrRE, ''))
       if (prop.dynamic) {
-        vm._bindDir('prop', el, {
-          arg: path,
-          expression: prop.value,
-          oneWay: prop.oneTime
-        }, propDef)
+        if (vm.$parent) {
+          vm._bindDir('prop', el, {
+            arg: path,
+            expression: prop.value,
+            oneWay: prop.oneTime
+          }, propDef)
+        } else {
+          _.warn(
+            'Cannot bind dynamic prop on a root instance' +
+            ' with no parent: ' + prop.name + '="' +
+            prop.raw + '"'
+          )
+        }
       } else {
         // just set once
-        vm.$set(path, prop.value)
+        vm.$set(path, prop.raw)
       }
     }
   }

+ 19 - 14
src/compiler/transclude.js

@@ -16,15 +16,17 @@ var transcludedFlagAttr = '__vue__transcluded'
  */
 
 module.exports = function transclude (el, options) {
-  if (options && options._asComponent) {
-    // extract container attributes to pass them down
-    // to compiler, because they need to be compiled in
-    // parent scope. we are mutating the options object here
-    // assuming the same object will be used for compile
-    // right after this.
+  // extract container attributes to pass them down
+  // to compiler, because they need to be compiled in
+  // parent scope. we are mutating the options object here
+  // assuming the same object will be used for compile
+  // right after this.
+  if (options) {
     options._containerAttrs = extractAttrs(el)
-    // Mark content nodes and attrs so that the compiler
-    // knows they should be compiled in parent scope.
+  }
+  // Mark content nodes and attrs so that the compiler
+  // knows they should be compiled in parent scope.
+  if (options && options._asComponent) {
     var i = el.childNodes.length
     while (i--) {
       var node = el.childNodes[i]
@@ -199,16 +201,19 @@ function insertContentAt (outlet, contents) {
  * determine whether an attribute is transcluded.
  *
  * @param {Element} el
+ * @return {Object}
  */
 
 function extractAttrs (el) {
-  var attrs = el.attributes
-  var res = {}
-  var i = attrs.length
-  while (i--) {
-    res[attrs[i].name] = attrs[i].value
+  if (el.nodeType === 1 && el.hasAttributes()) {
+    var attrs = el.attributes
+    var res = {}
+    var i = attrs.length
+    while (i--) {
+      res[attrs[i].name] = attrs[i].value
+    }
+    return res
   }
-  return res
 }
 
 /**

+ 1 - 1
src/directives/repeat.js

@@ -436,7 +436,7 @@ module.exports = {
     var cache = this.cache
     var id
     if (key || idKey) {
-      var id = idKey
+      id = idKey
         ? idKey === '$index'
           ? index
           : data[idKey]

+ 21 - 1
test/unit/specs/compiler/compile_spec.js

@@ -199,12 +199,32 @@ if (_.inBrowser) {
       expect(args[2].expression).toBe('this._applyFilters(a,null,[{"name":"filter"}],false)')
       expect(args[3]).toBe(def)
       // camelCase should've warn
-      expect(_.warn.calls.count()).toBe(1)
+      expect(hasWarned(_, 'using camelCase')).toBe(true)
       // literal and one time should've called vm.$set
       expect(vm.$set).toHaveBeenCalledWith('a', '1')
       expect(vm.$set).toHaveBeenCalledWith('someOtherAttr', '2')
     })
 
+    it('props on root instance', function () {
+      // temporarily remove vm.$parent
+      var parent = vm.$parent
+      vm.$parent = null
+      var options = _.mergeOptions(Vue.options, {
+        props: ['a', 'b']
+      })
+      var def = Vue.options.directives._prop
+      el.setAttribute('a', 'hi')
+      el.setAttribute('b', '{{hi}}')
+      transclude(el, options)
+      var linker = compile(el, options)
+      linker(vm, el)
+      expect(vm._bindDir.calls.count()).toBe(0)
+      expect(vm.$set).toHaveBeenCalledWith('a', 'hi')
+      expect(hasWarned(_, 'Cannot bind dynamic prop on a root')).toBe(true)
+      // restore parent mock
+      vm.$parent = parent
+    })
+
     it('DocumentFragment', function () {
       var frag = document.createDocumentFragment()
       frag.appendChild(el)