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: '

{{msg}}

' }) expect(child.$el.children[0].tagName).toBe('P') expect(child.$el.textContent).toBe('child message') }) it('fallback content with multiple named slots', () => { mount({ childTemplate: `

fallback a

fallback b
`, 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: `

fallback a

fallback b
`, 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('

foo

') vm.msg = 'bar' waitForUpdate(() => { expect(vm.$el.innerHTML).toBe('

bar

') }).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: '', 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( '
12
' ) 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: '
defaultnamed
' }, 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( `
foo
bar
foo
` ) vm.$refs.test.toggleEl = false waitForUpdate(() => { expect(vm.$el.innerHTML).toBe( `
foo
bar
foo
` ) }).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( '
hello
' ) }) 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( '
fallback
' ) }) // #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: ``, 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: ` ` } const vm = new Vue({ render: h => h(App) }).$mount() expect(vm.$el.innerHTML).toMatch(`App content for Bar#bar`) }) })