Skip to content

Commit e7083d0

Browse files
committed
fix scoped slots with dynamic slot names + force update for child components with scoped slots (fix #4779)
1 parent 6cbee6b commit e7083d0

File tree

8 files changed

+72
-13
lines changed

8 files changed

+72
-13
lines changed

Diff for: flow/component.js

+2
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ declare interface Component {
104104
_b: (data: any, value: any, asProp?: boolean) => VNodeData;
105105
// check custom keyCode
106106
_k: (eventKeyCode: number, key: string, builtInAlias: number | Array<number> | void) => boolean;
107+
// resolve scoped slots
108+
_u: (scopedSlots: Array<[string, Function]>) => { [key: string]: Function };
107109

108110
// allow dynamic method registration
109111
[key: string]: any

Diff for: src/compiler/codegen/index.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -273,17 +273,17 @@ function genInlineTemplate (el: ASTElement): ?string {
273273
}
274274

275275
function genScopedSlots (slots: { [key: string]: ASTElement }): string {
276-
return `scopedSlots:{${
276+
return `scopedSlots:_u([${
277277
Object.keys(slots).map(key => genScopedSlot(key, slots[key])).join(',')
278-
}}`
278+
}])`
279279
}
280280

281281
function genScopedSlot (key: string, el: ASTElement) {
282-
return `${key}:function(${String(el.attrsMap.scope)}){` +
282+
return `[${key},function(${String(el.attrsMap.scope)}){` +
283283
`return ${el.tag === 'template'
284284
? genChildren(el) || 'void 0'
285285
: genElement(el)
286-
}}`
286+
}}]`
287287
}
288288

289289
function genChildren (el: ASTElement, checkSkip?: boolean): string | void {

Diff for: src/compiler/parser/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ export function parse (
176176
processIfConditions(element, currentParent)
177177
} else if (element.slotScope) { // scoped slot
178178
currentParent.plain = false
179-
const name = element.slotTarget || 'default'
179+
const name = element.slotTarget || '"default"'
180180
;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
181181
} else {
182182
currentParent.children.push(element)

Diff for: src/core/instance/lifecycle.js

+13-3
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
import Watcher from '../observer/watcher'
44
import { createEmptyVNode } from '../vdom/vnode'
55
import { observerState } from '../observer/index'
6-
import { warn, validateProp, remove, noop } from '../util/index'
7-
import { resolveSlots } from './render-helpers/resolve-slots'
86
import { updateComponentListeners } from './events'
7+
import { resolveSlots } from './render-helpers/resolve-slots'
8+
import { warn, validateProp, remove, noop, emptyObject } from '../util/index'
99

1010
export let activeInstance: any = null
1111

@@ -120,13 +120,23 @@ export function lifecycleMixin (Vue: Class<Component>) {
120120
renderChildren: ?Array<VNode>
121121
) {
122122
const vm: Component = this
123-
const hasChildren = !!(vm.$options._renderChildren || renderChildren)
123+
124+
// determine whether component has slot children
125+
// we need to do this before overwriting $options._renderChildren
126+
const hasChildren = !!(
127+
renderChildren || // has new static slots
128+
vm.$options._renderChildren || // has old static slots
129+
parentVnode.data.scopedSlots || // has new scoped slots
130+
vm.$scopedSlots !== emptyObject // has old scoped slots
131+
)
132+
124133
vm.$options._parentVnode = parentVnode
125134
vm.$vnode = parentVnode // update vm's placeholder node without re-render
126135
if (vm._vnode) { // update child tree's parent
127136
vm._vnode.parent = parentVnode
128137
}
129138
vm.$options._renderChildren = renderChildren
139+
130140
// update props
131141
if (propsData && vm.$options.props) {
132142
observerState.shouldConvert = false

Diff for: src/core/instance/render-helpers/resolve-slots.js

+10
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,13 @@ export function resolveSlots (
3838
}
3939
return slots
4040
}
41+
42+
export function resolveScopedSlots (
43+
fns: Array<[string, Function]>
44+
): { [key: string]: Function } {
45+
const res = {}
46+
for (let i = 0; i < fns.length; i++) {
47+
res[fns[i][0]] = fns[i][1]
48+
}
49+
return res
50+
}

Diff for: src/core/instance/render.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
toNumber,
99
_toString,
1010
looseEqual,
11+
emptyObject,
1112
looseIndexOf,
1213
formatComponentName
1314
} from '../util/index'
@@ -21,11 +22,11 @@ import VNode, {
2122
import { createElement } from '../vdom/create-element'
2223
import { renderList } from './render-helpers/render-list'
2324
import { renderSlot } from './render-helpers/render-slot'
24-
import { resolveSlots } from './render-helpers/resolve-slots'
2525
import { resolveFilter } from './render-helpers/resolve-filter'
2626
import { checkKeyCodes } from './render-helpers/check-keycodes'
2727
import { bindObjectProps } from './render-helpers/bind-object-props'
2828
import { renderStatic, markOnce } from './render-helpers/render-static'
29+
import { resolveSlots, resolveScopedSlots } from './render-helpers/resolve-slots'
2930

3031
export function initRender (vm: Component) {
3132
vm.$vnode = null // the placeholder node in parent tree
@@ -34,7 +35,7 @@ export function initRender (vm: Component) {
3435
const parentVnode = vm.$options._parentVnode
3536
const renderContext = parentVnode && parentVnode.context
3637
vm.$slots = resolveSlots(vm.$options._renderChildren, renderContext)
37-
vm.$scopedSlots = {}
38+
vm.$scopedSlots = emptyObject
3839
// bind the createElement fn to this instance
3940
// so that we get proper render context inside it.
4041
// args order: tag, data, children, normalizationType, alwaysNormalize
@@ -65,9 +66,7 @@ export function renderMixin (Vue: Class<Component>) {
6566
}
6667
}
6768

68-
if (_parentVnode && _parentVnode.data.scopedSlots) {
69-
vm.$scopedSlots = _parentVnode.data.scopedSlots
70-
}
69+
vm.$scopedSlots = (_parentVnode && _parentVnode.data.scopedSlots) || emptyObject
7170

7271
if (staticRenderFns && !vm._staticTrees) {
7372
vm._staticTrees = []
@@ -124,4 +123,5 @@ export function renderMixin (Vue: Class<Component>) {
124123
Vue.prototype._b = bindObjectProps
125124
Vue.prototype._v = createTextVNode
126125
Vue.prototype._e = createEmptyVNode
126+
Vue.prototype._u = resolveScopedSlots
127127
}

Diff for: src/core/util/lang.js

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/* @flow */
22

3+
export const emptyObject = Object.freeze({})
4+
35
/**
46
* Check if a string starts with $ or _
57
*/

Diff for: test/unit/features/component/component-scoped-slot.spec.js

+35
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,41 @@ describe('Component scoped slot', () => {
325325
expect(vm.$el.innerHTML).toBe('<span>hello</span>')
326326
})
327327

328+
// #4779
329+
it('should support dynamic slot target', done => {
330+
const Child = {
331+
template: `
332+
<div>
333+
<slot name="a" msg="a" />
334+
<slot name="b" msg="b" />
335+
</div>
336+
`
337+
}
338+
339+
const vm = new Vue({
340+
data: {
341+
a: 'a',
342+
b: 'b'
343+
},
344+
template: `
345+
<child>
346+
<template :slot="a" scope="props">A {{ props.msg }}</template>
347+
<template :slot="b" scope="props">B {{ props.msg }}</template>
348+
</child>
349+
`,
350+
components: { Child }
351+
}).$mount()
352+
353+
expect(vm.$el.textContent.trim()).toBe('A a B b')
354+
355+
// switch slots
356+
vm.a = 'b'
357+
vm.b = 'a'
358+
waitForUpdate(() => {
359+
expect(vm.$el.textContent.trim()).toBe('B a A b')
360+
}).then(done)
361+
})
362+
328363
it('render function usage (JSX)', () => {
329364
const vm = new Vue({
330365
render (h) {

0 commit comments

Comments
 (0)