فهرست منبع

refactor component root compilation

Evan You 11 سال پیش
والد
کامیت
e65c8958ab

+ 130 - 100
src/compiler/compile.js

@@ -20,18 +20,9 @@ module.exports = compile
  */
 
 function compile (el, options, partial, transcluded) {
-  var isBlock = el.nodeType === 11
-  // link function for param attributes.
-  var params = options.paramAttributes
-  var paramsLinkFn = params && !partial && !transcluded && !isBlock
-    ? compileParamAttributes(el, params, options)
-    : null
   // link function for the node itself.
-  // if this is a block instance, we return a link function
-  // for the attributes found on the container, if any.
-  // options._containerAttrs are collected during transclusion.
-  var nodeLinkFn = isBlock
-    ? compileBlockContainer(options._containerAttrs, params, options)
+  var nodeLinkFn = options._asComponent && !partial
+    ? compileRoot(el, options)
     : compileNode(el, options)
   // link function for the childNodes
   var childLinkFn =
@@ -52,12 +43,12 @@ function compile (el, options, partial, transcluded) {
    */
 
   function compositeLinkFn (vm, el) {
+    // save original directive count before linking
+    // so we can capture the directives created during a
+    // partial compilation.
     var originalDirCount = vm._directives.length
     var parentOriginalDirCount =
       vm.$parent && vm.$parent._directives.length
-    if (paramsLinkFn) {
-      paramsLinkFn(vm, el)
-    }
     // cache childNodes before linking parent, fix #657
     var childNodes = _.toArray(el.childNodes)
     // if this is a transcluded compile, linkers need to be
@@ -109,36 +100,61 @@ function compile (el, options, partial, transcluded) {
 }
 
 /**
- * Compile the attributes found on a "block container" -
- * i.e. the container node in the parent tempate of a block
- * instance. We are only concerned with v-with and
- * paramAttributes here.
+ * Compile the root element of a component. There are
+ * 4 types of things to process here:
+ * 
+ * 1. paramAttributes 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
+ *    replace:true (child scope)
  *
- * @param {Object} attrs - a map of attr name/value pairs
- * @param {Array} params - param attributes list
+ * Also, if this is a block instance, we only need to
+ * compile 1 & 2 here.
+ *
+ * @param {Element} el
  * @param {Object} options
  * @return {Function}
  */
 
-function compileBlockContainer (attrs, params, options) {
-  if (!attrs) return null
-  var paramsLinkFn = params
-    ? compileParamAttributes(attrs, params, options)
+function compileRoot (el, options) {
+  var isBlock = el.nodeType === 11 // DocumentFragment
+  var containerAttrs = options._containerAttrs
+  var replacerAttrs = options._replacerAttrs
+  var params = options.paramAttributes
+  var paramsLinkFn, withLinkFn, parentLinkFn, replacerLinkFn
+  // 1. paramAttributes
+  paramsLinkFn = params
+    ? compileParamAttributes(el, containerAttrs, params, options)
     : null
-  var withVal = attrs[config.prefix + 'with']
-  var withLinkFn = null
+  // 2. v-with
+  var withName = config.prefix + 'with'
+  var withVal = containerAttrs && containerAttrs[withName]
   if (withVal) {
-    var descriptor = dirParser.parse(withVal)[0]
-    var def = options.directives['with']
-    withLinkFn = function (vm, el) {
-      vm._bindDir('with', el, descriptor, def)   
+    containerAttrs[withName] = null
+    withLinkFn = makeNodeLinkFn([{
+      name: 'with',
+      descriptors: dirParser.parse(withVal),
+      def: options.directives['with']
+    }])
+  }
+  if (!isBlock) {
+    // 3. container attributes
+    if (containerAttrs) {
+      parentLinkFn = compileDirectives(containerAttrs, options)
+    }
+    if (replacerAttrs) {
+      // 4. replacer attributes
+      replacerLinkFn = compileDirectives(replacerAttrs, options)
     }
   }
-  return function blockContainerLinkFn (vm) {
-    // explicitly passing null to the linkers
-    // since v-with doesn't need a real element
+  return function rootLinkFn (vm, el, host) {
+    // explicitly passing null to paramAttributes and v-with
+    // linkers because they don't need a real element
     if (paramsLinkFn) paramsLinkFn(vm, null)
     if (withLinkFn) withLinkFn(vm, null)
+    if (parentLinkFn) parentLinkFn(vm.$parent, el, host)
+    if (replacerLinkFn) replacerLinkFn(vm, el, host)
   }
 }
 
@@ -194,10 +210,7 @@ function compileElement (el, options) {
     linkFn = checkTerminalDirectives(el, options)
     // if not terminal, build normal link function
     if (!linkFn) {
-      var dirs = collectDirectives(el, options)
-      linkFn = dirs.length
-        ? makeNodeLinkFn(dirs)
-        : null
+      linkFn = compileDirectives(el, options)
     }
   }
   // if the element is a textarea, we need to interpolate
@@ -213,39 +226,6 @@ function compileElement (el, options) {
   return linkFn
 }
 
-/**
- * Build a link function for all directives on a single node.
- *
- * @param {Array} directives
- * @return {Function} directivesLinkFn
- */
-
-function makeNodeLinkFn (directives) {
-  return function nodeLinkFn (vm, el, host) {
-    // reverse apply because it's sorted low to high
-    var i = directives.length
-    var dir, j, k, target
-    while (i--) {
-      dir = directives[i]
-      // a directive can be transcluded if it's written
-      // on a component's container in its parent tempalte.
-      target = dir.transcluded
-        ? vm.$parent
-        : vm
-      if (dir._link) {
-        // custom link fn
-        dir._link(target, el)
-      } else {
-        k = dir.descriptors.length
-        for (j = 0; j < k; j++) {
-          target._bindDir(dir.name, el,
-            dir.descriptors[j], dir.def, host)
-        }
-      }
-    }
-  }
-}
-
 /**
  * Compile a textNode and return a nodeLinkFn.
  *
@@ -397,19 +377,19 @@ function makeChildLinkFn (linkFns) {
  * Compile param attributes on a root element and return
  * a paramAttributes link function.
  *
- * @param {Element|Object} el
- * @param {Array} attrs
+ * @param {Element|DocumentFragment} el
+ * @param {Object} attrs
+ * @param {Array} paramNames
  * @param {Object} options
  * @return {Function} paramsLinkFn
  */
 
-function compileParamAttributes (el, attrs, options) {
+function compileParamAttributes (el, attrs, paramNames, options) {
   var params = []
-  var isEl = el.nodeType
-  var i = attrs.length
+  var i = paramNames.length
   var name, value, param
   while (i--) {
-    name = attrs[i]
+    name = paramNames[i]
     if (/[A-Z]/.test(name)) {
       _.warn(
         'You seem to be using camelCase for a paramAttribute, ' +
@@ -419,7 +399,7 @@ function compileParamAttributes (el, attrs, options) {
         'http://vuejs.org/api/options.html#paramAttributes'
       )
     }
-    value = isEl ? el.getAttribute(name) : el[name]
+    value = attrs[name]
     if (value !== null) {
       param = {
         name: name,
@@ -427,7 +407,10 @@ function compileParamAttributes (el, attrs, options) {
       }
       var tokens = textParser.parse(value)
       if (tokens) {
-        if (isEl) el.removeAttribute(name)
+        if (el && el.nodeType === 1) {
+          el.removeAttribute(name)
+        }
+        attrs[name] = null
         if (tokens.length > 1) {
           _.warn(
             'Invalid param attribute binding: "' +
@@ -536,62 +519,109 @@ function makeTerminalNodeLinkFn (el, dirName, value, options) {
 }
 
 /**
- * Collect the directives on an element.
+ * Compile the directives on an element and return a linker.
  *
- * @param {Element} el
+ * @param {Element|Object} elOrAttrs
+ *        - could be an object of already-extracted
+ *          container attributes.
  * @param {Object} options
- * @return {Array}
+ * @return {Function}
  */
 
-function collectDirectives (el, options) {
-  var attrs = _.toArray(el.attributes)
+function compileDirectives (elOrAttrs, options) {
+  var attrs = _.isPlainObject(elOrAttrs)
+    ? mapToList(elOrAttrs)
+    : elOrAttrs.attributes
   var i = attrs.length
   var dirs = []
-  var attr, attrName, dir, dirName, dirDef, transcluded
+  var attr, name, value, dir, dirName, dirDef
   while (i--) {
     attr = attrs[i]
-    attrName = attr.name
-    transcluded =
-      options._transcludedAttrs &&
-      options._transcludedAttrs[attrName]
-    if (attrName.indexOf(config.prefix) === 0) {
-      dirName = attrName.slice(config.prefix.length)
+    name = attr.name
+    value = attr.value
+    if (value === null) continue
+    if (name.indexOf(config.prefix) === 0) {
+      dirName = name.slice(config.prefix.length)
       dirDef = options.directives[dirName]
       _.assertAsset(dirDef, 'directive', dirName)
       if (dirDef) {
         dirs.push({
           name: dirName,
-          descriptors: dirParser.parse(attr.value),
-          def: dirDef,
-          transcluded: transcluded
+          descriptors: dirParser.parse(value),
+          def: dirDef
         })
       }
     } else if (config.interpolate) {
-      dir = collectAttrDirective(el, attrName, attr.value,
-                                 options)
+      dir = collectAttrDirective(name, value, options)
       if (dir) {
-        dir.transcluded = transcluded
         dirs.push(dir)
       }
     }
   }
   // sort by priority, LOW to HIGH
-  dirs.sort(directiveComparator)
-  return dirs
+  if (dirs.length) {
+    dirs.sort(directiveComparator)
+    return makeNodeLinkFn(dirs)
+  }
+}
+
+/**
+ * Convert a map (Object) of attributes to an Array.
+ *
+ * @param {Object} map
+ * @return {Array}
+ */
+
+function mapToList (map) {
+  var list = []
+  for (var key in map) {
+    list.push({
+      name: key,
+      value: map[key]
+    })
+  }
+  return list
+}
+
+/**
+ * Build a link function for all directives on a single node.
+ *
+ * @param {Array} directives
+ * @return {Function} directivesLinkFn
+ */
+
+function makeNodeLinkFn (directives) {
+  return function nodeLinkFn (vm, el, host) {
+    // reverse apply because it's sorted low to high
+    var i = directives.length
+    var dir, j, k
+    while (i--) {
+      dir = directives[i]
+      if (dir._link) {
+        // custom link fn
+        dir._link(vm, el)
+      } else {
+        k = dir.descriptors.length
+        for (j = 0; j < k; j++) {
+          vm._bindDir(dir.name, el,
+            dir.descriptors[j], dir.def, host)
+        }
+      }
+    }
+  }
 }
 
 /**
  * Check an attribute for potential dynamic bindings,
  * and return a directive object.
  *
- * @param {Element} el
  * @param {String} name
  * @param {String} value
  * @param {Object} options
  * @return {Object}
  */
 
-function collectAttrDirective (el, name, value, options) {
+function collectAttrDirective (name, value, options) {
   var tokens = textParser.parse(value)
   if (tokens) {
     var def = options.directives.attr

+ 27 - 44
src/compiler/transclude.js

@@ -1,5 +1,4 @@
 var _ = require('../util')
-var config = require('../config')
 var templateParser = require('../parsers/template')
 var transcludedFlagAttr = '__vue__transcluded'
 
@@ -17,9 +16,12 @@ var transcludedFlagAttr = '__vue__transcluded'
 
 module.exports = function transclude (el, options) {
   if (options && options._asComponent) {
-    // mutating the options object here assuming the same
-    // object will be used for compile right after this
-    options._transcludedAttrs = extractAttrs(el.attributes)
+    // 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.
+    options._containerAttrs = extractAttrs(el)
     // Mark content nodes and attrs so that the compiler
     // knows they should be compiled in parent scope.
     var i = el.childNodes.length
@@ -72,24 +74,12 @@ function transcludeTemplate (el, options) {
     var rawContent = options._content || _.extractContent(el)
     if (options.replace) {
       if (frag.childNodes.length > 1) {
-        // this is a block instance which has no root node.
-        // however, the container in the parent template
-        // (which is replaced here) may contain v-with and
-        // paramAttributes that still need to be compiled
-        // for the child. we store all the container
-        // attributes on the options object and pass it down
-        // to the compiler.
-        var containerAttrs = options._containerAttrs = {}
-        var i = el.attributes.length
-        while (i--) {
-          var attr = el.attributes[i]
-          containerAttrs[attr.name] = attr.value
-        }
         transcludeContent(frag, rawContent)
         return frag
       } else {
         var replacer = frag.firstChild
-        copyAttrs(el, replacer, options)
+        options._replacerAttrs = extractAttrs(replacer)
+        mergeAttrs(el, replacer)
         transcludeContent(replacer, rawContent)
         return replacer
       }
@@ -194,49 +184,42 @@ function insertContentAt (outlet, contents) {
 
 /**
  * Helper to extract a component container's attribute names
- * into a map, and filtering out `v-with` in the process.
- * The resulting map will be used in compiler/compile to
+ * into a map. The resulting map will be used in compiler to
  * determine whether an attribute is transcluded.
  *
- * @param {NameNodeMap} attrs
+ * @param {Element} el
  */
 
-function extractAttrs (attrs) {
-  if (!attrs) return null
+function extractAttrs (el) {
+  var attrs = el.attributes
+  if (!attrs) return
   var res = {}
-  var vwith = config.prefix + 'with'
   var i = attrs.length
   while (i--) {
-    var name = attrs[i].name
-    if (name !== vwith) res[name] = true
+    res[attrs[i].name] = attrs[i].value
   }
   return res
 }
 
 /**
- * Copy attributes from one element to another.
+ * Merge the attributes of two elements, and make sure
+ * the class names are merged properly.
  *
  * @param {Element} from
  * @param {Element} to
- * @param {Object} options
  */
 
-function copyAttrs (from, to, options) {
-  if (from.hasAttributes()) {
-    var attrs = from.attributes
-    for (var i = 0, l = attrs.length; i < l; i++) {
-      var attr = attrs[i]
-      var name = attr.name
-      var value = attr.value
-      // do not overwrite
-      if (!to.hasAttribute(name)) {
-        to.setAttribute(name, value)
-      } else if (options._transcludedAttrs) {
-        // a parent container attribute is replaced by
-        // the replacer's attribute, we need to remove it
-        // from the list of transcluded attributes.
-        options._transcludedAttrs[name] = false
-      }
+function mergeAttrs (from, to) {
+  var attrs = from.attributes
+  var i = attrs.length
+  var name, value
+  while (i--) {
+    name = attrs[i].name
+    value = attrs[i].value
+    if (!to.hasAttribute(name)) {
+      to.setAttribute(name, value)
+    } else if (name === 'class') {
+      to.className = to.className + ' ' + value
     }
   }
 }

+ 3 - 1
src/directives/repeat.js

@@ -95,7 +95,9 @@ module.exports = {
       // important: transclude with no options, just
       // to ensure block start and block end
       this.template = transclude(this.template)
-      this._linkFn = compile(this.template, options)
+      var copy = _.extend({}, options)
+      copy._asComponent = false
+      this._linkFn = compile(this.template, copy)
     } else {
       this.asComponent = true
       // check inline-template

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

@@ -3,6 +3,7 @@ var _ = require('../../../../src/util')
 var dirParser = require('../../../../src/parsers/directive')
 var merge = require('../../../../src/util/merge-option')
 var compile = require('../../../../src/compiler/compile')
+var transclude = require('../../../../src/compiler/transclude')
 
 if (_.inBrowser) {
   describe('Compile', function () {
@@ -149,6 +150,7 @@ if (_.inBrowser) {
 
     it('param attributes', function () {
       var options = merge(Vue.options, {
+        _asComponent: true,
         paramAttributes: ['a', 'data-some-attr', 'some-other-attr', 'invalid', 'camelCase']
       })
       var def = Vue.options.directives['with']
@@ -156,13 +158,14 @@ if (_.inBrowser) {
       el.setAttribute('data-some-attr', '{{a}}')
       el.setAttribute('some-other-attr', '2')
       el.setAttribute('invalid', 'a {{b}} c') // invalid
+      transclude(el, options)
       var linker = compile(el, options)
       linker(vm, el)
       // should skip literal & invliad
       expect(vm._bindDir.calls.count()).toBe(1)
       var args = vm._bindDir.calls.argsFor(0)
       expect(args[0]).toBe('with')
-      expect(args[1]).toBe(el)
+      expect(args[1]).toBe(null)
       expect(args[2].arg).toBe('someAttr')
       expect(args[3]).toBe(def)
       // invalid and camelCase should've warn

+ 5 - 4
test/unit/specs/compiler/transclude_spec.js

@@ -119,14 +119,15 @@ if (_.inBrowser) {
       expect(res.lastChild.textContent).toBe('fallback c')
     })
 
-    it('replacer attr should overwrite container attr of same name', function () {
+    it('replacer attr should overwrite container attr of same name, except class should be merged', function () {
       el.setAttribute('class', 'test')
-      options.template = '<div class="other"></div>'
+      el.setAttribute('title', 'parent')
+      options.template = '<div class="other" title="child"></div>'
       options.replace = true
       options._asComponent = true
       var res = transclude(el, options)
-      expect(res.getAttribute('class')).toBe('other')
-      expect(options._transcludedAttrs['class']).toBe(false)
+      expect(res.getAttribute('class')).toBe('other test')
+      expect(res.getAttribute('title')).toBe('child')
     })
 
   })