Evan You 10 лет назад
Родитель
Сommit
3a0557a317

+ 5 - 0
src/core/instance/lifecycle.js

@@ -72,6 +72,11 @@ export function lifecycleMixin (Vue: Class<Component>) {
     const parentNode = vm.$options._parentVnode
     if (parentNode) {
       parentNode.elm = vm.$el
+      // update parent $el if the parent is HOC
+      // this is necessary because child is updated after parent
+      if (vm.$parent && parentNode === vm.$parent._vnode) {
+        vm.$parent.$el = vm.$el
+      }
     }
     if (vm._isMounted) {
       callHook(vm, 'updated')

+ 1 - 2
src/core/instance/render.js

@@ -184,9 +184,8 @@ function resolveSlots (
     const children = normalizeChildren(renderChildren)
     const slots = {}
     const defaultSlot = []
-    let i = children.length
     let name, child
-    while (i--) {
+    for (let i = 0, l = children.length; i < l; i++) {
       child = children[i]
       if ((name = child.data && child.data.slot)) {
         const slot = (slots[name] || (slots[name] = []))

+ 271 - 0
test/unit/features/component/component-slot.spec.js

@@ -0,0 +1,271 @@
+import Vue from 'vue'
+
+describe('Component slot', () => {
+  let vm, child
+  function mount (options) {
+    vm = new Vue({
+      data: {
+        msg: 'parent message'
+      },
+      template: `<div><test>${options.parentContent || ''}</test></div>`,
+      components: {
+        test: {
+          template: options.childTemplate,
+          data () {
+            return {
+              msg: 'child message'
+            }
+          }
+        }
+      }
+    }).$mount()
+    child = vm.$children[0]
+  }
+
+  it('no content', () => {
+    mount({
+      childTemplate: '<div><slot></slot></div>'
+    })
+    expect(child.$el.childNodes.length).toBe(0)
+  })
+
+  it('default content', done => {
+    mount({
+      childTemplate: '<div><slot></slot></div>',
+      parentContent: '<p>{{ msg }}</p>'
+    })
+    expect(child.$el.tagName).toBe('DIV')
+    expect(child.$el.children[0].tagName).toBe('P')
+    expect(child.$el.children[0].textContent).toBe('parent message')
+    vm.msg = 'changed'
+    waitForUpdate(() => {
+      expect(child.$el.children[0].textContent).toBe('changed')
+    }).then(done)
+  })
+
+  it('fallback content', () => {
+    mount({
+      childTemplate: '<div><slot><p>{{msg}}</p></slot></div>'
+    })
+    expect(child.$el.children[0].tagName).toBe('P')
+    expect(child.$el.textContent).toBe('child message')
+  })
+
+  it('fallback content with multiple named slots', () => {
+    mount({
+      childTemplate: `
+        <div>
+          <slot name="a"><p>fallback a</p></slot>
+          <slot name="b">fallback b</slot>
+        </div>
+      `,
+      parentContent: '<p slot="b">slot b</p>'
+    })
+    expect(child.$el.childNodes.length).toBe(2)
+    expect(child.$el.firstChild.textContent).toBe('fallback a')
+    expect(child.$el.lastChild.textContent).toBe('slot b')
+  })
+
+  it('fallback content with mixed named/unamed slots', () => {
+    mount({
+      childTemplate: `
+        <div>
+          <slot><p>fallback a</p></slot>
+          <slot name="b">fallback b</slot>
+        </div>
+      `,
+      parentContent: '<p slot="b">slot b</p>'
+    })
+    expect(child.$el.childNodes.length).toBe(2)
+    expect(child.$el.firstChild.textContent).toBe('fallback a')
+    expect(child.$el.lastChild.textContent).toBe('slot b')
+  })
+
+  it('selector matching multiple elements', () => {
+    mount({
+      childTemplate: '<div><slot name="t"></slot></div>',
+      parentContent: '<p slot="t">1</p><div></div><p slot="t">2</p>'
+    })
+    expect(child.$el.innerHTML).toBe('<p>1</p><p>2</p>')
+  })
+
+  it('default content should only render parts not selected', () => {
+    mount({
+      childTemplate: `
+        <div>
+          <slot name="a"></slot>
+          <slot></slot>
+          <slot name="b"></slot>
+        </div>
+      `,
+      parentContent: '<div>foo</div><p slot="a">1</p><p slot="b">2</p>'
+    })
+    expect(child.$el.innerHTML).toBe('<p>1</p><div>foo</div><p>2</p>')
+  })
+
+  it('name should only match children', function () {
+    mount({
+      childTemplate: `
+        <div>
+          <slot name="a"><p>fallback a</p></slot>
+          <slot name="b">fallback b</slot>
+          <slot name="c">fallback c</slot>
+        </div>
+      `,
+      parentContent: `
+        '<p slot="b">select b</p>
+        '<span><p slot="b">nested b</p></span>
+        '<span><p slot="c">nested c</p></span>
+      `
+    })
+    expect(child.$el.childNodes.length).toBe(3)
+    expect(child.$el.firstChild.textContent).toBe('fallback a')
+    expect(child.$el.childNodes[1].textContent).toBe('select b')
+    expect(child.$el.lastChild.textContent).toBe('fallback c')
+  })
+
+  it('should accept expressions in slot attribute and slot names', () => {
+    mount({
+      childTemplate: `<div><slot :name="'a'"></slot></div>`,
+      parentContent: `<p>one</p><p :slot="'a'">two</p>`
+    })
+    expect(child.$el.innerHTML).toBe('<p>two</p>')
+  })
+
+  it('slot inside v-if', done => {
+    const vm = new Vue({
+      data: {
+        a: 1,
+        b: 2,
+        show: true
+      },
+      template: '<test :show="show"><p slot="b">{{b}}</a><p>{{a}}</p></test>',
+      components: {
+        test: {
+          props: ['show'],
+          template: '<div v-if="show"><slot></slot><slot name="b"></slot></div>'
+        }
+      }
+    }).$mount()
+    expect(vm.$el.textContent).toBe('12')
+    vm.a = 2
+    waitForUpdate(() => {
+      expect(vm.$el.textContent).toBe('22')
+      vm.show = false
+    }).then(() => {
+      expect(vm.$el.textContent).toBe('')
+      vm.show = true
+      vm.a = 3
+    }).then(() => {
+      expect(vm.$el.textContent).toBe('32')
+    }).then(done)
+  })
+
+  it('slot inside v-for', () => {
+    mount({
+      childTemplate: '<div><slot v-for="i in 3" :name="i"></slot></div>',
+      parentContent: '<p v-for="i in 3" :slot="i">{{ i - 1 }}</p>'
+    })
+    expect(child.$el.innerHTML).toBe('<p>0</p><p>1</p><p>2</p>')
+  })
+
+  it('nested slots', done => {
+    const vm = new Vue({
+      template: '<test><test2><p>{{ msg }}</p></test2></test>',
+      data: {
+        msg: 'foo'
+      },
+      components: {
+        test: {
+          template: '<div><slot></slot></div>'
+        },
+        test2: {
+          template: '<div><slot></slot></div>'
+        }
+      }
+    }).$mount()
+    expect(vm.$el.innerHTML).toBe('<div><p>foo</p></div>')
+    vm.msg = 'bar'
+    waitForUpdate(() => {
+      expect(vm.$el.innerHTML).toBe('<div><p>bar</p></div>')
+    }).then(done)
+  })
+
+  it('v-if on inserted content', done => {
+    const vm = new Vue({
+      template: '<test><p v-if="ok">{{ msg }}</p></test>',
+      data: {
+        ok: true,
+        msg: 'hi'
+      },
+      components: {
+        test: {
+          template: '<div><slot>fallback</slot></div>'
+        }
+      }
+    }).$mount()
+    expect(vm.$el.innerHTML).toBe('<p>hi</p>')
+    vm.ok = false
+    waitForUpdate(() => {
+      expect(vm.$el.innerHTML).toBe('fallback')
+      vm.ok = true
+      vm.msg = 'bye'
+    }).then(() => {
+      expect(vm.$el.innerHTML).toBe('<p>bye</p>')
+    }).then(done)
+  })
+
+  it('template slot', function () {
+    const vm = new Vue({
+      template: '<test><template slot="test">hello</template></test>',
+      components: {
+        test: {
+          template: '<div><slot name="test"></slot> world</div>'
+        }
+      }
+    }).$mount()
+    expect(vm.$el.innerHTML).toBe('hello world')
+  })
+
+  it('combined with v-for', () => {
+    const vm = new Vue({
+      template: '<div><test v-for="i in 3">{{ i }}</test></div>',
+      components: {
+        test: {
+          template: '<div><slot></slot></div>'
+        }
+      }
+    }).$mount()
+    expect(vm.$el.innerHTML).toBe('<div>1</div><div>2</div><div>3</div>')
+  })
+
+  it('inside template v-if', () => {
+    mount({
+      childTemplate: `
+        <div>
+          <template v-if="true"><slot></slot></template>
+        </div>
+      `,
+      parentContent: 'foo'
+    })
+    expect(child.$el.innerHTML).toBe('foo')
+  })
+
+  it('default slot should use fallback content if has only whitespace', () => {
+    Vue.config.preserveWhitespace = true
+    mount({
+      childTemplate: `
+        <div>
+          <slot name="first"><p>first slot</p></slot>
+          <slot><p>this is the default slot</p></slot>
+          <slot name="second"><p>second named slot</p></slot>
+        </div>
+      `,
+      parentContent: `<div slot="first">1</div> <div slot="second">2</div>`
+    })
+    expect(child.$el.innerHTML).toBe(
+      '<div>1</div> <p>this is the default slot</p> <div>2</div>'
+    )
+    Vue.config.preserveWhitespace = false
+  })
+})