import Vue from 'vue'
describe('Component slot', () => {
let vm, child
function mount(options) {
vm = new Vue({
data: {
msg: 'parent message'
},
template: `
${options.parentContent || ''}
`,
components: {
test: {
template: options.childTemplate,
data() {
return {
msg: 'child message'
}
}
}
}
}).$mount()
child = vm.$children[0]
}
it('no content', () => {
mount({
childTemplate: '
'
})
expect(child.$el.childNodes.length).toBe(0)
})
it('default slot', done => {
mount({
childTemplate: '
',
parentContent: '{{ msg }}
'
})
expect(child.$el.tagName).toBe('DIV')
expect(child.$el.children[0].tagName).toBe('P')
expect(child.$el.children[0].textContent).toBe('parent message')
vm.msg = 'changed'
waitForUpdate(() => {
expect(child.$el.children[0].textContent).toBe('changed')
}).then(done)
})
it('named slot', done => {
mount({
childTemplate: '
',
parentContent: '{{ msg }}
'
})
expect(child.$el.tagName).toBe('DIV')
expect(child.$el.children[0].tagName).toBe('P')
expect(child.$el.children[0].textContent).toBe('parent message')
vm.msg = 'changed'
waitForUpdate(() => {
expect(child.$el.children[0].textContent).toBe('changed')
}).then(done)
})
it('named slot with 0 as a number', done => {
mount({
childTemplate: '
',
parentContent: '{{ msg }}
'
})
expect(child.$el.tagName).toBe('DIV')
expect(child.$el.children[0].tagName).toBe('P')
expect(child.$el.children[0].textContent).toBe('parent message')
vm.msg = 'changed'
waitForUpdate(() => {
expect(child.$el.children[0].textContent).toBe('changed')
}).then(done)
})
it('fallback content', () => {
mount({
childTemplate: ''
})
expect(child.$el.children[0].tagName).toBe('P')
expect(child.$el.textContent).toBe('child message')
})
it('fallback content with multiple named slots', () => {
mount({
childTemplate: `
`,
parentContent: 'slot b
'
})
expect(child.$el.children.length).toBe(2)
expect(child.$el.children[0].textContent).toBe('fallback a')
expect(child.$el.children[1].textContent).toBe('slot b')
})
it('fallback content with mixed named/unnamed slots', () => {
mount({
childTemplate: `
`,
parentContent: 'slot b
'
})
expect(child.$el.children.length).toBe(2)
expect(child.$el.children[0].textContent).toBe('fallback a')
expect(child.$el.children[1].textContent).toBe('slot b')
})
it('it should work with previous versions of the templates', () => {
const Test = {
render() {
const _vm = this
// const _h = _vm.$createElement;
const _c = _vm._self._c || vm._h
return _c(
'div',
[_vm._t('default', [_c('p', [_vm._v('slot default')])])],
2
)
}
}
let vm = new Vue({
template: ` `,
components: { Test }
}).$mount()
expect(vm.$el.textContent).toBe('slot default')
vm = new Vue({
template: `custom content `,
components: { Test }
}).$mount()
expect(vm.$el.textContent).toBe('custom content')
})
it('fallback content should not be evaluated when the parent is providing it', () => {
const test = vi.fn()
const vm = new Vue({
template: 'slot default ',
components: {
test: {
template: '{{test()}}
',
methods: {
test() {
test()
return 'test'
}
}
}
}
}).$mount()
expect(vm.$el.textContent).toBe('slot default')
expect(test).not.toHaveBeenCalled()
})
it('selector matching multiple elements', () => {
mount({
childTemplate: '
',
parentContent: '1
2
'
})
expect(child.$el.innerHTML).toBe('1
2
')
})
it('default content should only render parts not selected', () => {
mount({
childTemplate: `
`,
parentContent: 'foo
1
2
'
})
expect(child.$el.innerHTML).toBe('1
foo
2
')
})
it('name should only match children', function () {
mount({
childTemplate: `
fallback a
fallback b
fallback c
`,
parentContent: `
'select b
'nested b
'nested c
`
})
expect(child.$el.children.length).toBe(3)
expect(child.$el.children[0].textContent).toBe('fallback a')
expect(child.$el.children[1].textContent).toBe('select b')
expect(child.$el.children[2].textContent).toBe('fallback c')
})
it('should accept expressions in slot attribute and slot names', () => {
mount({
childTemplate: `
`,
parentContent: `one
two
`
})
expect(child.$el.innerHTML).toBe('two
')
})
it('slot inside v-if', done => {
const vm = new Vue({
data: {
a: 1,
b: 2,
show: true
},
template: '{{b}}
{{a}}
',
components: {
test: {
props: ['show'],
template: '
'
}
}
}).$mount()
expect(vm.$el.textContent).toBe('12')
vm.a = 2
waitForUpdate(() => {
expect(vm.$el.textContent).toBe('22')
vm.show = false
})
.then(() => {
expect(vm.$el.textContent).toBe('')
vm.show = true
vm.a = 3
})
.then(() => {
expect(vm.$el.textContent).toBe('32')
})
.then(done)
})
it('slot inside v-for', () => {
mount({
childTemplate: '
',
parentContent: '{{ i - 1 }}
'
})
expect(child.$el.innerHTML).toBe('0
1
2
')
})
it('nested slots', done => {
const vm = new Vue({
template: '{{ msg }}
',
data: {
msg: 'foo'
},
components: {
test: {
template: '
'
},
test2: {
template: '
'
}
}
}).$mount()
expect(vm.$el.innerHTML).toBe('')
vm.msg = 'bar'
waitForUpdate(() => {
expect(vm.$el.innerHTML).toBe('')
}).then(done)
})
it('v-if on inserted content', done => {
const vm = new Vue({
template: '{{ msg }}
',
data: {
ok: true,
msg: 'hi'
},
components: {
test: {
template: 'fallback
'
}
}
}).$mount()
expect(vm.$el.innerHTML).toBe('hi
')
vm.ok = false
waitForUpdate(() => {
expect(vm.$el.innerHTML).toBe('fallback')
vm.ok = true
vm.msg = 'bye'
})
.then(() => {
expect(vm.$el.innerHTML).toBe('bye
')
})
.then(done)
})
it('template slot', function () {
const vm = new Vue({
template: 'hello ',
components: {
test: {
template: ' world
'
}
}
}).$mount()
expect(vm.$el.innerHTML).toBe('hello world')
})
it('combined with v-for', () => {
const vm = new Vue({
template: '{{ i }}
',
components: {
test: {
template: '
'
}
}
}).$mount()
expect(vm.$el.innerHTML).toBe('1
2
3
')
})
it('inside template v-if', () => {
mount({
childTemplate: `
`,
parentContent: 'foo'
})
expect(child.$el.innerHTML).toBe('foo')
})
it('default slot should use fallback content if has only whitespace', () => {
mount({
childTemplate: `
first slot
this is the default slot
second named slot
`,
parentContent: `1
2
2+
`
})
expect(child.$el.innerHTML).toBe(
'1
this is the default slot
2
2+
'
)
})
it('programmatic access to $slots', () => {
const vm = new Vue({
template: 'A
C
B
',
components: {
test: {
render() {
expect(this.$slots.a.length).toBe(1)
expect(this.$slots.a[0].tag).toBe('p')
expect(this.$slots.a[0].children.length).toBe(1)
expect(this.$slots.a[0].children[0].text).toBe('A')
expect(this.$slots.b.length).toBe(1)
expect(this.$slots.b[0].tag).toBe('p')
expect(this.$slots.b[0].children.length).toBe(1)
expect(this.$slots.b[0].children[0].text).toBe('B')
expect(this.$slots.default.length).toBe(1)
expect(this.$slots.default[0].tag).toBe('div')
expect(this.$slots.default[0].children.length).toBe(1)
expect(this.$slots.default[0].children[0].text).toBe('C')
return this.$slots.default[0]
}
}
}
}).$mount()
expect(vm.$el.tagName).toBe('DIV')
expect(vm.$el.textContent).toBe('C')
})
it('warn if user directly returns array', () => {
new Vue({
template: '
',
components: {
test: {
render() {
return this.$slots.foo
}
}
}
}).$mount()
expect(
'Render function should return a single root node'
).toHaveBeenWarned()
})
// #3254
it('should not keep slot name when passed further down', () => {
const vm = new Vue({
template: 'foo ',
components: {
test: {
template: ' ',
components: {
child: {
template: `
`
}
}
}
}
}).$mount()
expect(vm.$el.querySelector('.default').textContent).toBe('foo')
expect(vm.$el.querySelector('.named').textContent).toBe('')
})
it('should not keep slot name when passed further down (nested)', () => {
const vm = new Vue({
template: 'foo ',
components: {
wrap: {
template: '
'
},
test: {
template: ' ',
components: {
child: {
template: `
`
}
}
}
}
}).$mount()
expect(vm.$el.querySelector('.default').textContent).toBe('foo')
expect(vm.$el.querySelector('.named').textContent).toBe('')
})
it('should not keep slot name when passed further down (functional)', () => {
const child = {
template: `
`
}
const vm = new Vue({
template: 'foo ',
components: {
test: {
functional: true,
render(h, ctx) {
const slots = ctx.slots()
return h(child, slots.foo)
}
}
}
}).$mount()
expect(vm.$el.querySelector('.default').textContent).toBe('foo')
expect(vm.$el.querySelector('.named').textContent).toBe('')
})
// #3400
it('named slots should be consistent across re-renders', done => {
const vm = new Vue({
template: `
foo
`,
components: {
comp: {
data() {
return { a: 1 }
},
template: ` {{ a }}
`
}
}
}).$mount()
expect(vm.$el.textContent).toBe('foo1')
vm.$children[0].a = 2
waitForUpdate(() => {
expect(vm.$el.textContent).toBe('foo2')
}).then(done)
})
// #3437
it('should correctly re-create components in slot', done => {
const calls: any[] = []
const vm = new Vue({
template: `
`,
components: {
comp: {
data() {
return { ok: true }
},
template: `
`
},
child: {
template: 'child
',
created() {
calls.push(1)
},
destroyed() {
calls.push(2)
}
}
}
}).$mount()
expect(calls).toEqual([1])
vm.$refs.child.ok = false
waitForUpdate(() => {
expect(calls).toEqual([1, 2])
vm.$refs.child.ok = true
})
.then(() => {
expect(calls).toEqual([1, 2, 1])
vm.$refs.child.ok = false
})
.then(() => {
expect(calls).toEqual([1, 2, 1, 2])
})
.then(done)
})
it('should support duplicate slots', done => {
const vm = new Vue({
template: `
{{ n }}
`,
data: {
n: 1
},
components: {
foo: {
data() {
return { ok: true }
},
template: `
`
}
}
}).$mount()
expect(vm.$el.innerHTML).toBe(
`1
1
1
`
)
vm.n++
waitForUpdate(() => {
expect(vm.$el.innerHTML).toBe(
`2
2
2
`
)
vm.n++
})
.then(() => {
expect(vm.$el.innerHTML).toBe(
`3
3
3
`
)
vm.$refs.foo.ok = false
})
.then(() => {
expect(vm.$el.innerHTML).toBe(
`3
3
`
)
vm.n++
vm.$refs.foo.ok = true
})
.then(() => {
expect(vm.$el.innerHTML).toBe(
`4
4
4
`
)
})
.then(done)
})
// #3518
it('events should not break when slot is toggled by v-if', done => {
const spy = vi.fn()
const vm = new Vue({
template: `hi
`,
methods: {
test: spy
},
components: {
test: {
data: () => ({
toggle: true
}),
template: `
`
}
}
}).$mount()
document.body.appendChild(vm.$el)
expect(vm.$el.textContent).toBe('hi')
vm.$children[0].toggle = false
waitForUpdate(() => {
vm.$children[0].toggle = true
})
.then(() => {
global.triggerEvent(vm.$el.querySelector('.click'), 'click')
expect(spy).toHaveBeenCalled()
})
.then(() => {
document.body.removeChild(vm.$el)
})
.then(done)
})
it('renders static tree with text', () => {
const vm = new Vue({
template: ``,
components: {
test: {
template: '
'
}
}
})
vm.$mount()
expect('Error when rendering root').not.toHaveBeenWarned()
})
// #3872
it('functional component as slot', () => {
const vm = new Vue({
template: `
one
two
`,
components: {
parent: {
template: `
`
},
child: {
functional: true,
render(h, { slots }) {
return h('div', slots().default)
}
}
}
}).$mount()
expect(vm.$el.innerHTML.trim()).toBe('two
one
')
})
// #4209
it('slot of multiple text nodes should not be infinitely merged', done => {
const wrap = {
template: `foo `,
components: {
inner: {
data: () => ({ a: 1 }),
template: `{{a}}
`
}
}
}
const vm = new Vue({
template: `bar `,
components: { wrap }
}).$mount()
expect(vm.$el.textContent).toBe('1foobar')
vm.$refs.wrap.$refs.inner.a++
waitForUpdate(() => {
expect(vm.$el.textContent).toBe('2foobar')
}).then(done)
})
// #4315
it('functional component passing slot content to stateful child component', done => {
const ComponentWithSlots = {
render(h) {
return h('div', this.$slots.slot1)
}
}
const FunctionalComp = {
functional: true,
render(h) {
return h(ComponentWithSlots, [h('span', { slot: 'slot1' }, 'foo')])
}
}
const vm = new Vue({
data: { n: 1 },
render(h) {
return h('div', [this.n, h(FunctionalComp)])
}
}).$mount()
expect(vm.$el.textContent).toBe('1foo')
vm.n++
waitForUpdate(() => {
// should not lose named slot
expect(vm.$el.textContent).toBe('2foo')
}).then(done)
})
it('the elements of slot should be updated correctly', done => {
const vm = new Vue({
data: { n: 1 },
template:
'{{ i }}
',
components: {
test: {
template: '
'
}
}
}).$mount()
expect(vm.$el.innerHTML).toBe('1
')
const input = vm.$el.querySelector('input')
input.value = 'b'
vm.n++
waitForUpdate(() => {
expect(vm.$el.innerHTML).toBe(
'1 2
'
)
expect(vm.$el.querySelector('input')).toBe(input)
expect(vm.$el.querySelector('input').value).toBe('b')
}).then(done)
})
// GitHub issue #5888
it('should resolve correctly slot with keep-alive', () => {
const vm = new Vue({
template: `
`,
components: {
container: {
template:
'default named
'
},
child: {
template: 'foo '
}
}
}).$mount()
expect(vm.$el.innerHTML).toBe('defaultfoo
')
})
// #6372, #6915
it('should handle nested components in slots properly', done => {
const TestComponent = {
template: `
`,
data() {
return {
toggleEl: true
}
}
}
const vm = new Vue({
template: `
`,
components: {
TestComponent,
foo: {
template: `foo
`
},
bar: {
template: `bar
`
}
}
}).$mount()
expect(vm.$el.innerHTML).toBe(
` `
)
vm.$refs.test.toggleEl = false
waitForUpdate(() => {
expect(vm.$el.innerHTML).toBe(
` `
)
}).then(done)
})
it('should preserve slot attribute if not absorbed by a Vue component', () => {
const vm = new Vue({
template: `
`
}).$mount()
expect(vm.$el.children[0].getAttribute('slot')).toBe('foo')
})
it('passing a slot down as named slot', () => {
const Bar = {
template: `
`
}
const Foo = {
components: { Bar },
template: `
`
}
const vm = new Vue({
components: { Foo },
template: `hello
`
}).$mount()
expect(vm.$el.innerHTML).toBe(
''
)
})
it('fallback content for named template slot', () => {
const Bar = {
template: `fallback
`
}
const Foo = {
components: { Bar },
template: `
`
}
const vm = new Vue({
components: { Foo },
template: `
`
}).$mount()
expect(vm.$el.innerHTML).toBe(
''
)
})
// #7106
it('should not lose functional slot across renders', done => {
const One = {
data: () => ({
foo: true
}),
render(h) {
this.foo
return h('div', this.$slots.slot)
}
}
const Two = {
render(h) {
return h('span', this.$slots.slot)
}
}
const Three = {
functional: true,
render: (h, { children }) => h('span', children)
}
const vm = new Vue({
template: `
hello
`,
components: { One, Two, Three }
}).$mount()
expect(vm.$el.textContent).toBe('hello')
// trigger re-render of
vm.$refs.one.foo = false
waitForUpdate(() => {
// should still be there
expect(vm.$el.textContent).toBe('hello')
}).then(done)
})
it('should allow passing named slots as raw children down multiple layers of functional component', () => {
const CompB = {
functional: true,
render(h, { slots }) {
return slots().foo
}
}
const CompA = {
functional: true,
render(h, { children }) {
return h(CompB, children)
}
}
const vm = new Vue({
components: {
CompA
},
template: `
foo
`
}).$mount()
expect(vm.$el.textContent).toBe('foo')
})
// #7817
it('should not match wrong named slot in functional component on re-render', done => {
const Functional = {
functional: true,
render: (h, ctx) => ctx.slots().default
}
const Stateful = {
data() {
return { ok: true }
},
render(h) {
this.ok // register dep
return h('div', [h(Functional, this.$slots.named)])
}
}
const vm = new Vue({
template: `foo
`,
components: { Stateful }
}).$mount()
expect(vm.$el.textContent).toBe('foo')
vm.$refs.stateful.ok = false
waitForUpdate(() => {
expect(vm.$el.textContent).toBe('foo')
}).then(done)
})
// #7975
it('should update named slot correctly when its position in the tree changed', done => {
const ChildComponent = {
template: '{{ message }} ',
props: ['message']
}
let parentVm
const ParentComponent = {
template: `
`,
data() {
return {
alter: true
}
},
mounted() {
parentVm = this
}
}
const vm = new Vue({
template: `
`,
components: {
ChildComponent,
ParentComponent
},
data() {
return {
message: 1
}
}
}).$mount()
expect(vm.$el.firstChild.innerHTML).toBe(
'1 '
)
parentVm.alter = false
waitForUpdate(() => {
vm.message = 2
})
.then(() => {
expect(vm.$el.firstChild.innerHTML).toBe('2 ')
})
.then(done)
})
// #12102
it('v-if inside scoped slot', () => {
const vm = new Vue({
template: `a b `,
components: {
test: {
template: `
`
}
}
}).$mount()
expect(vm.$el.innerHTML).toBe(`b `)
})
// regression 2.7.0-alpha.4
it('passing scoped slots through nested parent chain', () => {
const Foo = {
template: `
foo default
`
}
const Bar = {
components: { Foo },
template: ` `
}
const App = {
components: { Bar },
template: `
App content for Bar#bar
`
}
const vm = new Vue({
render: h => h(App)
}).$mount()
expect(vm.$el.innerHTML).toMatch(`App content for Bar#bar`)
})
})