Skip to content

Commit

Permalink
fix(scoped-slots): ensure $scopedSlots calls always return Arrays
Browse files Browse the repository at this point in the history
Also allow render functions to return an Array of a single element.
Close #8056
  • Loading branch information
yyx990803 committed Dec 26, 2018
1 parent d747469 commit c7c13c2
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 15 deletions.
7 changes: 4 additions & 3 deletions src/core/instance/render-helpers/resolve-slots.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,11 @@ export function resolveScopedSlots (
): { [key: string]: Function } {
res = res || {}
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 {
res[fns[i].key] = fns[i].fn
res[slot.key] = slot.fn
}
}
return res
Expand Down
7 changes: 6 additions & 1 deletion src/core/instance/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { createElement } from '../vdom/create-element'
import { installRenderHelpers } from './render-helpers/index'
import { resolveSlots } from './render-helpers/resolve-slots'
import { normalizeScopedSlots } from '../vdom/helpers/normalize-scoped-slots'
import VNode, { createEmptyVNode } from '../vdom/vnode'

import { isUpdatingChildComponent } from './lifecycle'
Expand Down Expand Up @@ -63,7 +64,7 @@ export function renderMixin (Vue: Class<Component>) {
const { render, _parentVnode } = vm.$options

if (_parentVnode) {
vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject
vm.$scopedSlots = normalizeScopedSlots(_parentVnode.data.scopedSlots)
}

// set parent vnode. this allows render functions to have access
Expand All @@ -89,6 +90,10 @@ export function renderMixin (Vue: Class<Component>) {
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
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
Expand Down
3 changes: 2 additions & 1 deletion src/core/vdom/create-functional-component.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { createElement } from './create-element'
import { resolveInject } from '../instance/inject'
import { normalizeChildren } from '../vdom/helpers/normalize-children'
import { resolveSlots } from '../instance/render-helpers/resolve-slots'
import { normalizeScopedSlots } from '../vdom/helpers/normalize-scoped-slots'
import { installRenderHelpers } from '../instance/render-helpers/index'

import {
Expand Down Expand Up @@ -56,7 +57,7 @@ export function FunctionalRenderContext (
this.$options = options
// pre-resolve slots for renderSlot()
this.$slots = this.slots()
this.$scopedSlots = data.scopedSlots || emptyObject
this.$scopedSlots = normalizeScopedSlots(data.scopedSlots)
}

if (options._scopeId) {
Expand Down
25 changes: 25 additions & 0 deletions src/core/vdom/helpers/normalize-scoped-slots.js
Original file line number Diff line number Diff line change
@@ -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
}
}
36 changes: 28 additions & 8 deletions test/unit/features/component/component-scoped-slot.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -395,11 +395,9 @@ describe('Component scoped slot', () => {
return { msg: 'hello' }
},
render (h) {
return h('div', [
this.$scopedSlots.item({
text: this.msg
})
])
return h('div', this.$scopedSlots.item({
text: this.msg
}))
}
}
}
Expand All @@ -425,16 +423,38 @@ describe('Component scoped slot', () => {
return { msg: 'hello' }
},
render (h) {
return h('div', [
this.$scopedSlots.default({ msg: this.msg })
])
return h('div', this.$scopedSlots.default({ msg: this.msg }))
}
}
}
}).$mount()
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
it('should support dynamic slot target', done => {
const Child = {
Expand Down
4 changes: 2 additions & 2 deletions test/unit/features/component/component-slot.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -327,11 +327,11 @@ describe('Component slot', () => {

it('warn if user directly returns array', () => {
new Vue({
template: '<test><div></div></test>',
template: '<test><div slot="foo"></div><div slot="foo"></div></test>',
components: {
test: {
render () {
return this.$slots.default
return this.$slots.foo
}
}
}
Expand Down

0 comments on commit c7c13c2

Please sign in to comment.