Skip to content

Commit

Permalink
fix scoped slots with dynamic slot names + force update for child com…
Browse files Browse the repository at this point in the history
…ponents with scoped slots (fix #4779)
  • Loading branch information
yyx990803 committed Jan 24, 2017
1 parent 6cbee6b commit e7083d0
Show file tree
Hide file tree
Showing 8 changed files with 72 additions and 13 deletions.
2 changes: 2 additions & 0 deletions flow/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ declare interface Component {
_b: (data: any, value: any, asProp?: boolean) => VNodeData;
// check custom keyCode
_k: (eventKeyCode: number, key: string, builtInAlias: number | Array<number> | void) => boolean;
// resolve scoped slots
_u: (scopedSlots: Array<[string, Function]>) => { [key: string]: Function };

// allow dynamic method registration
[key: string]: any
Expand Down
8 changes: 4 additions & 4 deletions src/compiler/codegen/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,17 +273,17 @@ function genInlineTemplate (el: ASTElement): ?string {
}

function genScopedSlots (slots: { [key: string]: ASTElement }): string {
return `scopedSlots:{${
return `scopedSlots:_u([${
Object.keys(slots).map(key => genScopedSlot(key, slots[key])).join(',')
}}`
}])`
}

function genScopedSlot (key: string, el: ASTElement) {
return `${key}:function(${String(el.attrsMap.scope)}){` +
return `[${key},function(${String(el.attrsMap.scope)}){` +
`return ${el.tag === 'template'
? genChildren(el) || 'void 0'
: genElement(el)
}}`
}}]`
}

function genChildren (el: ASTElement, checkSkip?: boolean): string | void {
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/parser/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export function parse (
processIfConditions(element, currentParent)
} else if (element.slotScope) { // scoped slot
currentParent.plain = false
const name = element.slotTarget || 'default'
const name = element.slotTarget || '"default"'
;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
} else {
currentParent.children.push(element)
Expand Down
16 changes: 13 additions & 3 deletions src/core/instance/lifecycle.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import Watcher from '../observer/watcher'
import { createEmptyVNode } from '../vdom/vnode'
import { observerState } from '../observer/index'
import { warn, validateProp, remove, noop } from '../util/index'
import { resolveSlots } from './render-helpers/resolve-slots'
import { updateComponentListeners } from './events'
import { resolveSlots } from './render-helpers/resolve-slots'
import { warn, validateProp, remove, noop, emptyObject } from '../util/index'

export let activeInstance: any = null

Expand Down Expand Up @@ -120,13 +120,23 @@ export function lifecycleMixin (Vue: Class<Component>) {
renderChildren: ?Array<VNode>
) {
const vm: Component = this
const hasChildren = !!(vm.$options._renderChildren || renderChildren)

// determine whether component has slot children
// we need to do this before overwriting $options._renderChildren
const hasChildren = !!(
renderChildren || // has new static slots
vm.$options._renderChildren || // has old static slots
parentVnode.data.scopedSlots || // has new scoped slots
vm.$scopedSlots !== emptyObject // has old scoped slots
)

vm.$options._parentVnode = parentVnode
vm.$vnode = parentVnode // update vm's placeholder node without re-render
if (vm._vnode) { // update child tree's parent
vm._vnode.parent = parentVnode
}
vm.$options._renderChildren = renderChildren

// update props
if (propsData && vm.$options.props) {
observerState.shouldConvert = false
Expand Down
10 changes: 10 additions & 0 deletions src/core/instance/render-helpers/resolve-slots.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,13 @@ export function resolveSlots (
}
return slots
}

export function resolveScopedSlots (
fns: Array<[string, Function]>
): { [key: string]: Function } {
const res = {}
for (let i = 0; i < fns.length; i++) {
res[fns[i][0]] = fns[i][1]
}
return res
}
10 changes: 5 additions & 5 deletions src/core/instance/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
toNumber,
_toString,
looseEqual,
emptyObject,
looseIndexOf,
formatComponentName
} from '../util/index'
Expand All @@ -21,11 +22,11 @@ import VNode, {
import { createElement } from '../vdom/create-element'
import { renderList } from './render-helpers/render-list'
import { renderSlot } from './render-helpers/render-slot'
import { resolveSlots } from './render-helpers/resolve-slots'
import { resolveFilter } from './render-helpers/resolve-filter'
import { checkKeyCodes } from './render-helpers/check-keycodes'
import { bindObjectProps } from './render-helpers/bind-object-props'
import { renderStatic, markOnce } from './render-helpers/render-static'
import { resolveSlots, resolveScopedSlots } from './render-helpers/resolve-slots'

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

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

if (staticRenderFns && !vm._staticTrees) {
vm._staticTrees = []
Expand Down Expand Up @@ -124,4 +123,5 @@ export function renderMixin (Vue: Class<Component>) {
Vue.prototype._b = bindObjectProps
Vue.prototype._v = createTextVNode
Vue.prototype._e = createEmptyVNode
Vue.prototype._u = resolveScopedSlots
}
2 changes: 2 additions & 0 deletions src/core/util/lang.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/* @flow */

export const emptyObject = Object.freeze({})

/**
* Check if a string starts with $ or _
*/
Expand Down
35 changes: 35 additions & 0 deletions test/unit/features/component/component-scoped-slot.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,41 @@ describe('Component scoped slot', () => {
expect(vm.$el.innerHTML).toBe('<span>hello</span>')
})

// #4779
it('should support dynamic slot target', done => {
const Child = {
template: `
<div>
<slot name="a" msg="a" />
<slot name="b" msg="b" />
</div>
`
}

const vm = new Vue({
data: {
a: 'a',
b: 'b'
},
template: `
<child>
<template :slot="a" scope="props">A {{ props.msg }}</template>
<template :slot="b" scope="props">B {{ props.msg }}</template>
</child>
`,
components: { Child }
}).$mount()

expect(vm.$el.textContent.trim()).toBe('A a B b')

// switch slots
vm.a = 'b'
vm.b = 'a'
waitForUpdate(() => {
expect(vm.$el.textContent.trim()).toBe('B a A b')
}).then(done)
})

it('render function usage (JSX)', () => {
const vm = new Vue({
render (h) {
Expand Down

0 comments on commit e7083d0

Please sign in to comment.