Skip to content

Commit

Permalink
feat: support slot-props and its shorthand
Browse files Browse the repository at this point in the history
See #9306 for more details.
  • Loading branch information
yyx990803 committed Jan 12, 2019
1 parent 4f61d5b commit 584e89d
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 40 deletions.
118 changes: 78 additions & 40 deletions src/compiler/parser/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ let postTransforms
let platformIsPreTag
let platformMustUseProp
let platformGetTagNamespace
let maybeComponent

export function createASTElement (
tag: string,
Expand Down Expand Up @@ -74,6 +75,8 @@ export function parse (
platformIsPreTag = options.isPreTag || no
platformMustUseProp = options.mustUseProp || no
platformGetTagNamespace = options.getTagNamespace || no
const isReservedTag = options.isReservedTag || no
maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag)

transforms = pluckModuleFunction(options.modules, 'transformNode')
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
Expand Down Expand Up @@ -390,7 +393,8 @@ export function processElement (
)

processRef(element)
processSlot(element)
processSlotContent(element)
processSlotOutlet(element)
processComponent(element)
for (let i = 0; i < transforms.length; i++) {
element = transforms[i](element, options) || element
Expand Down Expand Up @@ -542,7 +546,79 @@ function processOnce (el) {
}
}

function processSlot (el) {
// handle content being passed to a component as slot,
// e.g. <template slot="xxx">, <div slot-scope="xxx">
function processSlotContent (el) {
let slotScope
if (el.tag === 'template') {
slotScope = getAndRemoveAttr(el, 'scope')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && slotScope) {
warn(
`the "scope" attribute for scoped slots have been deprecated and ` +
`replaced by "slot-scope" since 2.5. The new "slot-scope" attribute ` +
`can also be used on plain elements in addition to <template> to ` +
`denote scoped slots.`,
el.rawAttrsMap['scope'],
true
)
}
el.slotScope = (
slotScope ||
getAndRemoveAttr(el, 'slot-scope') ||
// new in 2.6: slot-props and its shorthand works the same as slot-scope
// when used on <template> containers
getAndRemoveAttr(el, 'slot-props') ||
getAndRemoveAttr(el, '()')
)
} else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && el.attrsMap['v-for']) {
warn(
`Ambiguous combined usage of slot-scope and v-for on <${el.tag}> ` +
`(v-for takes higher priority). Use a wrapper <template> for the ` +
`scoped slot to make it clearer.`,
el.rawAttrsMap['slot-scope'],
true
)
}
el.slotScope = slotScope
} else {
// 2.6: slot-props on component, denotes default slot
slotScope = getAndRemoveAttr(el, 'slot-props') || getAndRemoveAttr(el, '()')
if (slotScope) {
if (process.env.NODE_ENV !== 'production' && !maybeComponent(el)) {
warn(
`slot-props cannot be used on non-component elements.`,
el.rawAttrsMap['slot-props'] || el.rawAttrsMap['()']
)
}
// add the component's children to its default slot
const slots = el.scopedSlots || (el.scopedSlots = {})
const slotContainer = slots[`"default"`] = createASTElement('template', [], el)
slotContainer.children = el.children
slotContainer.slotScope = slotScope
// remove children as they are returned from scopedSlots now
el.children = []
// mark el non-plain so data gets generated
el.plain = false
}
}

// slot="xxx"
const slotTarget = getBindingAttr(el, 'slot')
if (slotTarget) {
el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
// preserve slot as an attribute for native shadow DOM compat
// only for non-scoped slots.
if (el.tag !== 'template' && !el.slotScope) {
addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'))
}
}
}

// handle <slot/> outlets
function processSlotOutlet (el) {
if (el.tag === 'slot') {
el.slotName = getBindingAttr(el, 'name')
if (process.env.NODE_ENV !== 'production' && el.key) {
Expand All @@ -553,44 +629,6 @@ function processSlot (el) {
getRawBindingAttr(el, 'key')
)
}
} else {
let slotScope
if (el.tag === 'template') {
slotScope = getAndRemoveAttr(el, 'scope')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && slotScope) {
warn(
`the "scope" attribute for scoped slots have been deprecated and ` +
`replaced by "slot-scope" since 2.5. The new "slot-scope" attribute ` +
`can also be used on plain elements in addition to <template> to ` +
`denote scoped slots.`,
el.rawAttrsMap['scope'],
true
)
}
el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope')
} else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && el.attrsMap['v-for']) {
warn(
`Ambiguous combined usage of slot-scope and v-for on <${el.tag}> ` +
`(v-for takes higher priority). Use a wrapper <template> for the ` +
`scoped slot to make it clearer.`,
el.rawAttrsMap['slot-scope'],
true
)
}
el.slotScope = slotScope
}
const slotTarget = getBindingAttr(el, 'slot')
if (slotTarget) {
el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
// preserve slot as an attribute for native shadow DOM compat
// only for non-scoped slots.
if (el.tag !== 'template' && !el.slotScope) {
addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'))
}
}
}
}

Expand Down
101 changes: 101 additions & 0 deletions test/unit/features/component/component-scoped-slot.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -631,4 +631,105 @@ describe('Component scoped slot', () => {
expect(vm.$el.innerHTML).toBe('<p>hello</p>')
}).then(done)
})

// new in 2.6
describe('slot-props syntax', () => {
const Foo = {
render(h) {
return h('div', [
this.$scopedSlots.default && this.$scopedSlots.default('from foo default'),
this.$scopedSlots.one && this.$scopedSlots.one('from foo one'),
this.$scopedSlots.two && this.$scopedSlots.two('from foo two')
])
}
}

const Bar = {
render(h) {
return this.$scopedSlots.default && this.$scopedSlots.default('from bar')[0]
}
}

const Baz = {
render(h) {
return this.$scopedSlots.default && this.$scopedSlots.default('from baz')[0]
}
}

function runSuite(syntax) {
it('default slot', () => {
const vm = new Vue({
template: `<foo ${syntax}="foo">{{ foo }}<div>{{ foo }}</div></foo>`,
components: { Foo }
}).$mount()
expect(vm.$el.innerHTML).toBe(`from foo default<div>from foo default</div>`)
})

it('nested default slots', () => {
const vm = new Vue({
template: `
<foo ${syntax}="foo">
<bar ${syntax}="bar">
<baz ${syntax}="baz">
{{ foo }} | {{ bar }} | {{ baz }}
</baz>
</bar>
</foo>
`,
components: { Foo, Bar, Baz }
}).$mount()
expect(vm.$el.innerHTML.trim()).toBe(`from foo default | from bar | from baz`)
})

it('default + named slots', () => {
const vm = new Vue({
template: `
<foo ()="foo">
{{ foo }}
<template slot="one" ${syntax}="one">
{{ one }}
</template>
<template slot="two" ${syntax}="two">
{{ two }}
</template>
</foo>
`,
components: { Foo }
}).$mount()
expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`from foo default from foo one from foo two`)
})

it('nested + named + default slots', () => {
const vm = new Vue({
template: `
<foo>
<template slot="one" ${syntax}="one">
<bar ${syntax}="bar">
{{ one }} {{ bar }}
</bar>
</template>
<template slot="two" ${syntax}="two">
<baz ${syntax}="baz">
{{ two }} {{ baz }}
</baz>
</template>
</foo>
`,
components: { Foo, Bar, Baz }
}).$mount()
expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`from foo one from bar from foo two from baz`)
})

it('should warn slot-props usage on non-component elements', () => {
const vm = new Vue({
template: `<div ${syntax}="foo"/>`
}).$mount()
expect(`slot-props cannot be used on non-component elements`).toHaveBeenWarned()
})
}

// run tests for both full syntax and shorthand
runSuite('slot-props')
runSuite('()')
})
})

0 comments on commit 584e89d

Please sign in to comment.