Просмотр исходного кода

fix(scoped-slots): ensure $scopedSlots calls always return Arrays

Also allow render functions to return an Array of a single element.
Close #8056
Evan You 7 лет назад
Родитель
Сommit
c7c13c2a15

+ 4 - 3
src/core/instance/render-helpers/resolve-slots.js

@@ -55,10 +55,11 @@ export function resolveScopedSlots (
 ): { [key: string]: Function } {
 ): { [key: string]: Function } {
   res = res || {}
   res = res || {}
   for (let i = 0; i < fns.length; i++) {
   for (let i = 0; i < fns.length; i++) {
-    if (Array.isArray(fns[i])) {
-      resolveScopedSlots(fns[i], res)
+    const slot = fns[i]
+    if (Array.isArray(slot)) {
+      resolveScopedSlots(slot, res)
     } else {
     } else {
-      res[fns[i].key] = fns[i].fn
+      res[slot.key] = slot.fn
     }
     }
   }
   }
   return res
   return res

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

@@ -11,6 +11,7 @@ import {
 import { createElement } from '../vdom/create-element'
 import { createElement } from '../vdom/create-element'
 import { installRenderHelpers } from './render-helpers/index'
 import { installRenderHelpers } from './render-helpers/index'
 import { resolveSlots } from './render-helpers/resolve-slots'
 import { resolveSlots } from './render-helpers/resolve-slots'
+import { normalizeScopedSlots } from '../vdom/helpers/normalize-scoped-slots'
 import VNode, { createEmptyVNode } from '../vdom/vnode'
 import VNode, { createEmptyVNode } from '../vdom/vnode'
 
 
 import { isUpdatingChildComponent } from './lifecycle'
 import { isUpdatingChildComponent } from './lifecycle'
@@ -63,7 +64,7 @@ export function renderMixin (Vue: Class<Component>) {
     const { render, _parentVnode } = vm.$options
     const { render, _parentVnode } = vm.$options
 
 
     if (_parentVnode) {
     if (_parentVnode) {
-      vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject
+      vm.$scopedSlots = normalizeScopedSlots(_parentVnode.data.scopedSlots)
     }
     }
 
 
     // set parent vnode. this allows render functions to have access
     // set parent vnode. this allows render functions to have access
@@ -89,6 +90,10 @@ export function renderMixin (Vue: Class<Component>) {
         vnode = vm._vnode
         vnode = vm._vnode
       }
       }
     }
     }
+    // if the returned array contains only a single node, allow it
+    if (Array.isArray(vnode) && vnode.length === 1) {
+      vnode = vnode[0]
+    }
     // return empty vnode in case the render function errored out
     // return empty vnode in case the render function errored out
     if (!(vnode instanceof VNode)) {
     if (!(vnode instanceof VNode)) {
       if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
       if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {

+ 2 - 1
src/core/vdom/create-functional-component.js

@@ -5,6 +5,7 @@ import { createElement } from './create-element'
 import { resolveInject } from '../instance/inject'
 import { resolveInject } from '../instance/inject'
 import { normalizeChildren } from '../vdom/helpers/normalize-children'
 import { normalizeChildren } from '../vdom/helpers/normalize-children'
 import { resolveSlots } from '../instance/render-helpers/resolve-slots'
 import { resolveSlots } from '../instance/render-helpers/resolve-slots'
+import { normalizeScopedSlots } from '../vdom/helpers/normalize-scoped-slots'
 import { installRenderHelpers } from '../instance/render-helpers/index'
 import { installRenderHelpers } from '../instance/render-helpers/index'
 
 
 import {
 import {
@@ -56,7 +57,7 @@ export function FunctionalRenderContext (
     this.$options = options
     this.$options = options
     // pre-resolve slots for renderSlot()
     // pre-resolve slots for renderSlot()
     this.$slots = this.slots()
     this.$slots = this.slots()
-    this.$scopedSlots = data.scopedSlots || emptyObject
+    this.$scopedSlots = normalizeScopedSlots(data.scopedSlots)
   }
   }
 
 
   if (options._scopeId) {
   if (options._scopeId) {

+ 25 - 0
src/core/vdom/helpers/normalize-scoped-slots.js

@@ -0,0 +1,25 @@
+/* @flow */
+
+import { emptyObject } from 'core/util/index'
+
+export function normalizeScopedSlots (slots: { [key: string]: Function }) {
+  if (!slots) {
+    return emptyObject
+  } else if (slots._normalized) {
+    return slots
+  } else {
+    const res = {}
+    for (const key in slots) {
+      res[key] = normalizeScopedSlot(slots[key])
+    }
+    res._normalized = true
+    return res
+  }
+}
+
+function normalizeScopedSlot(fn: Function) {
+  return scope => {
+    const res = fn(scope)
+    return Array.isArray(res) ? res : res ? [res] : res
+  }
+}

+ 28 - 8
test/unit/features/component/component-scoped-slot.spec.js

@@ -395,11 +395,9 @@ describe('Component scoped slot', () => {
             return { msg: 'hello' }
             return { msg: 'hello' }
           },
           },
           render (h) {
           render (h) {
-            return h('div', [
-              this.$scopedSlots.item({
-                text: this.msg
-              })
-            ])
+            return h('div', this.$scopedSlots.item({
+              text: this.msg
+            }))
           }
           }
         }
         }
       }
       }
@@ -425,9 +423,7 @@ describe('Component scoped slot', () => {
             return { msg: 'hello' }
             return { msg: 'hello' }
           },
           },
           render (h) {
           render (h) {
-            return h('div', [
-              this.$scopedSlots.default({ msg: this.msg })
-            ])
+            return h('div', this.$scopedSlots.default({ msg: this.msg }))
           }
           }
         }
         }
       }
       }
@@ -435,6 +431,30 @@ describe('Component scoped slot', () => {
     expect(vm.$el.innerHTML).toBe('<span>hello</span>')
     expect(vm.$el.innerHTML).toBe('<span>hello</span>')
   })
   })
 
 
+  it('render function usage (default, as root)', () => {
+    const vm = new Vue({
+      render (h) {
+        return h('test', [
+          props => h('span', [props.msg])
+        ])
+      },
+      components: {
+        test: {
+          data () {
+            return { msg: 'hello' }
+          },
+          render (h) {
+            const res = this.$scopedSlots.default({ msg: this.msg })
+            // all scoped slots should be normalized into arrays
+            expect(Array.isArray(res)).toBe(true)
+            return res
+          }
+        }
+      }
+    }).$mount()
+    expect(vm.$el.outerHTML).toBe('<span>hello</span>')
+  })
+
   // #4779
   // #4779
   it('should support dynamic slot target', done => {
   it('should support dynamic slot target', done => {
     const Child = {
     const Child = {

+ 2 - 2
test/unit/features/component/component-slot.spec.js

@@ -327,11 +327,11 @@ describe('Component slot', () => {
 
 
   it('warn if user directly returns array', () => {
   it('warn if user directly returns array', () => {
     new Vue({
     new Vue({
-      template: '<test><div></div></test>',
+      template: '<test><div slot="foo"></div><div slot="foo"></div></test>',
       components: {
       components: {
         test: {
         test: {
           render () {
           render () {
-            return this.$slots.default
+            return this.$slots.foo
           }
           }
         }
         }
       }
       }