Pārlūkot izejas kodu

call attach/detach for dynamicly created components inside if block

Evan You 11 gadi atpakaļ
vecāks
revīzija
154c845d0a

+ 3 - 3
src/compiler/compile.js

@@ -341,7 +341,7 @@ function compileNodeList (nodeList, options) {
  */
 
 function makeChildLinkFn (linkFns) {
-  return function childLinkFn (vm, nodes) {
+  return function childLinkFn (vm, nodes, host) {
     var node, nodeLinkFn, childrenLinkFn
     for (var i = 0, n = 0, l = linkFns.length; i < l; n++) {
       node = nodes[n]
@@ -350,10 +350,10 @@ function makeChildLinkFn (linkFns) {
       // cache childNodes before linking parent, fix #657
       var childNodes = _.toArray(node.childNodes)
       if (nodeLinkFn) {
-        nodeLinkFn(vm, node)
+        nodeLinkFn(vm, node, host)
       }
       if (childrenLinkFn) {
-        childrenLinkFn(vm, childNodes)
+        childrenLinkFn(vm, childNodes, host)
       }
     }
   }

+ 43 - 0
src/compiler/transclude.js

@@ -1,5 +1,7 @@
 var _ = require('../util')
+var config = require('../config')
 var templateParser = require('../parsers/template')
+var transcludedFlagAttr = '__vue__transcluded'
 
 /**
  * Process an element or a DocumentFragment based on a
@@ -14,6 +16,27 @@ var templateParser = require('../parsers/template')
  */
 
 module.exports = function transclude (el, options) {
+  if (options && options._asComponent) {
+    // Mark content nodes and attrs so that the compiler
+    // knows they should be compiled in parent scope.
+    options._transcludedAttrs = extractAttrs(el.attributes)
+    var i = el.childNodes.length
+    while (i--) {
+      var node = el.childNodes[i]
+      if (node.nodeType === 1) {
+        node.setAttribute(transcludedFlagAttr, '')
+      } else if (node.nodeType === 3 && node.data.trim()) {
+        // wrap transcluded textNodes in spans, because
+        // raw textNodes can't be persisted through clones
+        // by attaching attributes.
+        var wrapper = document.createElement('span')
+        wrapper.textContent = node.data
+        wrapper.setAttribute('__vue__wrap', '')
+        wrapper.setAttribute(transcludedFlagAttr, '')
+        el.replaceChild(wrapper, node)
+      }
+    }
+  }
   // for template tags, what we want is its content as
   // a documentFragment (for block instances)
   if (el.tagName === 'TEMPLATE') {
@@ -153,4 +176,24 @@ function insertContentAt (outlet, contents) {
     parent.insertBefore(contents[i], outlet)
   }
   parent.removeChild(outlet)
+}
+
+/**
+ * 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
+ * determine whether an attribute is transcluded.
+ *
+ * @param {NameNodeMap} attrs
+ */
+
+function extractAttrs (attrs) {
+  var res = {}
+  var vwith = config.prefix + 'with'
+  var i = attrs.length
+  while (i--) {
+    var name = attrs[i].name
+    if (name !== vwith) res[name] = true
+  }
+  return res
 }

+ 55 - 20
src/directives/if.js

@@ -50,46 +50,81 @@ module.exports = {
   // NOTE: this function is shared in v-partial
   compile: function (frag) {
     var vm = this.vm
-    var originalChildLength = vm._children.length
-    var originalParentChildLength = vm.$parent &&
-      vm.$parent._children.length
     // the linker is not guaranteed to be present because
     // this function might get called by v-partial 
     this.unlink = this.linker
       ? this.linker(vm, frag)
       : vm.$compile(frag)
     transition.blockAppend(frag, this.end, vm)
-    this.children = vm._children.slice(originalChildLength)
-    if (vm.$parent) {
-      this.children = this.children.concat(
-        vm.$parent._children.slice(originalParentChildLength)
-      )
-    }
-    if (this.children.length && _.inDoc(vm.$el)) {
-      this.children.forEach(function (child) {
-        child._callHook('attached')
-      })
+    // call attached for all the child components created
+    // during the compilation
+    if (_.inDoc(vm.$el)) {
+      var children = this.getContainedComponents()
+      if (children) children.forEach(callAttach)
     }
   },
 
   // NOTE: this function is shared in v-partial
   teardown: function () {
     if (!this.unlink) return
-    transition.blockRemove(this.start, this.end, this.vm)
-    if (this.children && _.inDoc(this.vm.$el)) {
-      this.children.forEach(function (child) {
-        if (!child._isDestroyed) {
-          child._callHook('detached')
-        }
-      })
+    // collect children beforehand
+    var children
+    if (_.inDoc(this.vm.$el)) {
+      children = this.getContainedComponents()
     }
+    transition.blockRemove(this.start, this.end, this.vm)
+    if (children) children.forEach(callDetach)
     this.unlink()
     this.unlink = null
   },
 
+  // NOTE: this function is shared in v-partial
+  getContainedComponents: function () {
+    var vm = this.vm
+    var start = this.start.nextSibling
+    var end = this.end
+    var selfCompoents =
+      vm._children.length &&
+      vm._children.filter(contains)
+    var transComponents =
+      vm._transCpnts &&
+      vm._transCpnts.filter(contains)
+
+    function contains (c) {
+      var cur = start
+      var next
+      while (next !== end) {
+        next = cur.nextSibling
+        if (cur.contains(c.$el)) {
+          return true
+        }
+        cur = next
+      }
+      return false
+    }
+
+    return selfCompoents
+      ? transComponents
+        ? selfCompoents.concat(transComponents)
+        : selfCompoents
+      : transComponents
+  },
+
   // NOTE: this function is shared in v-partial
   unbind: function () {
     if (this.unlink) this.unlink()
   }
 
+}
+
+function callAttach (child) {
+  if (!child._isAttached) {
+    child._callHook('attached')
+  }
+}
+
+function callDetach (child) {
+  if (child._isAttached) {
+    child._callHook('detached')
+  }
 }

+ 1 - 0
src/directives/partial.js

@@ -9,6 +9,7 @@ module.exports = {
   // same logic reuse from v-if
   compile: vIf.compile,
   teardown: vIf.teardown,
+  getContainedComponents: vIf.getContainedComponents,
   unbind: vIf.unbind,
 
   bind: function () {

+ 1 - 43
src/instance/compile.js

@@ -1,9 +1,7 @@
 var _ = require('../util')
-var config = require('../config')
 var Directive = require('../directive')
 var compile = require('../compiler/compile')
 var transclude = require('../compiler/transclude')
-var transcludedFlagAttr = '__vue__transcluded'
 
 /**
  * Transclude, compile and link element.
@@ -21,30 +19,10 @@ var transcludedFlagAttr = '__vue__transcluded'
 exports._compile = function (el) {
   var options = this.$options
   if (options._linkFn) {
+    // pre-transcluded with linker, just use it
     this._initElement(el)
     options._linkFn(this, el)
   } else {
-    if (options._asComponent) {
-      // Mark content nodes and attrs so that the compiler
-      // knows they should be compiled in parent scope.
-      options._transcludedAttrs = extractAttrs(el.attributes)
-      var i = el.childNodes.length
-      while (i--) {
-        var node = el.childNodes[i]
-        if (node.nodeType === 1) {
-          node.setAttribute(transcludedFlagAttr, '')
-        } else if (node.nodeType === 3 && node.data.trim()) {
-          // wrap transcluded textNodes in spans, because
-          // raw textNodes can't be persisted through clones
-          // by attaching attributes.
-          var wrapper = document.createElement('span')
-          wrapper.textContent = node.data
-          wrapper.setAttribute('__vue__wrap', '')
-          wrapper.setAttribute(transcludedFlagAttr, '')
-          el.replaceChild(wrapper, node)
-        }
-      }
-    }
     // transclude and init element
     // transclude can potentially replace original
     // so we need to keep reference
@@ -186,24 +164,4 @@ exports._cleanup = function () {
   this._callHook('destroyed')
   // turn off all instance listeners.
   this.$off()
-}
-
-/**
- * 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
- * determine whether an attribute is transcluded.
- *
- * @param {NameNodeMap} attrs
- */
-
-function extractAttrs (attrs) {
-  var res = {}
-  var vwith = config.prefix + 'with'
-  var i = attrs.length
-  while (i--) {
-    var name = attrs[i].name
-    if (name !== vwith) res[name] = true
-  }
-  return res
 }

+ 75 - 0
test/unit/specs/directives/if_spec.js

@@ -235,9 +235,84 @@ if (_.inBrowser) {
       vm.show = false
       _.nextTick(function () {
         expect(detachSpy).toHaveBeenCalled()
+        document.body.removeChild(el)
         done()
       })
     })
 
+    it('call attach/detach for dynamicly created components inside if block', function (done) {
+      document.body.appendChild(el)
+      var attachSpy = jasmine.createSpy('attached')
+      var detachSpy = jasmine.createSpy('detached')
+      var vm = new Vue({
+        el: el,
+        data: {
+          show: true,
+          list: [{a:0}]
+        },
+        template:
+          '<div v-component="outer">' +
+            '<div>' + // an extra layer to test components deep inside the tree
+              '<div v-repeat="list" v-component="transcluded"></div>' +
+            '</div>' +
+          '</div>',
+        components: {
+          outer: {
+            template:
+              '<div v-if="$parent.show">' +
+                '<content></content>' +
+              '</div>' +
+              // this is to test that compnents that are not in the if block
+              // should not fire attach/detach when v-if toggles
+              '<div v-component="transcluded"></div>'
+          },
+          transcluded: {
+            template: '{{a}}',
+            attached: attachSpy,
+            detached: detachSpy
+          }
+        }
+      })
+      assertMarkup()
+      expect(attachSpy.calls.count()).toBe(2)
+      vm.show = false
+      _.nextTick(function () {
+        assertMarkup()
+        expect(detachSpy.calls.count()).toBe(1)
+        vm.list.push({a:1})
+        vm.show = true
+        _.nextTick(function () {
+          assertMarkup()
+          expect(attachSpy.calls.count()).toBe(2 + 2)
+          vm.list.push({a:2})
+          vm.show = false
+          _.nextTick(function () {
+            assertMarkup()
+            expect(attachSpy.calls.count()).toBe(2 + 2 + 1)
+            expect(detachSpy.calls.count()).toBe(1 + 3)
+            document.body.removeChild(el)
+            done()
+          })
+        })
+      })
+
+      function assertMarkup () {
+        var showBlock = vm.show
+          ? '<div><div>' +
+              vm.list.map(function (o) {
+                return '<div>' + o.a + '</div>'
+              }).join('') + '<!--v-repeat-->' +
+            '</div></div>'
+          : ''
+        var markup = '<div>' +
+          '<!--v-if-start-->' +
+            showBlock +
+          '<!--v-if-end-->' +
+          '<div></div><!--v-component-->' +
+        '</div><!--v-component-->'
+        expect(el.innerHTML).toBe(markup)
+      }
+    })
+
   })
 }