component-slot.spec.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. import Vue from 'vue'
  2. describe('Component slot', () => {
  3. let vm, child
  4. function mount (options) {
  5. vm = new Vue({
  6. data: {
  7. msg: 'parent message'
  8. },
  9. template: `<div><test>${options.parentContent || ''}</test></div>`,
  10. components: {
  11. test: {
  12. template: options.childTemplate,
  13. data () {
  14. return {
  15. msg: 'child message'
  16. }
  17. }
  18. }
  19. }
  20. }).$mount()
  21. child = vm.$children[0]
  22. }
  23. it('no content', () => {
  24. mount({
  25. childTemplate: '<div><slot></slot></div>'
  26. })
  27. expect(child.$el.childNodes.length).toBe(0)
  28. })
  29. it('default slot', done => {
  30. mount({
  31. childTemplate: '<div><slot></slot></div>',
  32. parentContent: '<p>{{ msg }}</p>'
  33. })
  34. expect(child.$el.tagName).toBe('DIV')
  35. expect(child.$el.children[0].tagName).toBe('P')
  36. expect(child.$el.children[0].textContent).toBe('parent message')
  37. vm.msg = 'changed'
  38. waitForUpdate(() => {
  39. expect(child.$el.children[0].textContent).toBe('changed')
  40. }).then(done)
  41. })
  42. it('named slot', done => {
  43. mount({
  44. childTemplate: '<div><slot name="test"></slot></div>',
  45. parentContent: '<p slot="test">{{ msg }}</p>'
  46. })
  47. expect(child.$el.tagName).toBe('DIV')
  48. expect(child.$el.children[0].tagName).toBe('P')
  49. expect(child.$el.children[0].textContent).toBe('parent message')
  50. vm.msg = 'changed'
  51. waitForUpdate(() => {
  52. expect(child.$el.children[0].textContent).toBe('changed')
  53. }).then(done)
  54. })
  55. it('fallback content', () => {
  56. mount({
  57. childTemplate: '<div><slot><p>{{msg}}</p></slot></div>'
  58. })
  59. expect(child.$el.children[0].tagName).toBe('P')
  60. expect(child.$el.textContent).toBe('child message')
  61. })
  62. it('fallback content with multiple named slots', () => {
  63. mount({
  64. childTemplate: `
  65. <div>
  66. <slot name="a"><p>fallback a</p></slot>
  67. <slot name="b">fallback b</slot>
  68. </div>
  69. `,
  70. parentContent: '<p slot="b">slot b</p>'
  71. })
  72. expect(child.$el.children.length).toBe(2)
  73. expect(child.$el.children[0].textContent).toBe('fallback a')
  74. expect(child.$el.children[1].textContent).toBe('slot b')
  75. })
  76. it('fallback content with mixed named/unamed slots', () => {
  77. mount({
  78. childTemplate: `
  79. <div>
  80. <slot><p>fallback a</p></slot>
  81. <slot name="b">fallback b</slot>
  82. </div>
  83. `,
  84. parentContent: '<p slot="b">slot b</p>'
  85. })
  86. expect(child.$el.children.length).toBe(2)
  87. expect(child.$el.children[0].textContent).toBe('fallback a')
  88. expect(child.$el.children[1].textContent).toBe('slot b')
  89. })
  90. it('selector matching multiple elements', () => {
  91. mount({
  92. childTemplate: '<div><slot name="t"></slot></div>',
  93. parentContent: '<p slot="t">1</p><div></div><p slot="t">2</p>'
  94. })
  95. expect(child.$el.innerHTML).toBe('<p>1</p><p>2</p>')
  96. })
  97. it('default content should only render parts not selected', () => {
  98. mount({
  99. childTemplate: `
  100. <div>
  101. <slot name="a"></slot>
  102. <slot></slot>
  103. <slot name="b"></slot>
  104. </div>
  105. `,
  106. parentContent: '<div>foo</div><p slot="a">1</p><p slot="b">2</p>'
  107. })
  108. expect(child.$el.innerHTML).toBe('<p>1</p> <div>foo</div> <p>2</p>')
  109. })
  110. it('name should only match children', function () {
  111. mount({
  112. childTemplate: `
  113. <div>
  114. <slot name="a"><p>fallback a</p></slot>
  115. <slot name="b"><p>fallback b</p></slot>
  116. <slot name="c"><p>fallback c</p></slot>
  117. </div>
  118. `,
  119. parentContent: `
  120. '<p slot="b">select b</p>
  121. '<span><p slot="b">nested b</p></span>
  122. '<span><p slot="c">nested c</p></span>
  123. `
  124. })
  125. expect(child.$el.children.length).toBe(3)
  126. expect(child.$el.children[0].textContent).toBe('fallback a')
  127. expect(child.$el.children[1].textContent).toBe('select b')
  128. expect(child.$el.children[2].textContent).toBe('fallback c')
  129. })
  130. it('should accept expressions in slot attribute and slot names', () => {
  131. mount({
  132. childTemplate: `<div><slot :name="'a'"></slot></div>`,
  133. parentContent: `<p>one</p><p :slot="'a'">two</p>`
  134. })
  135. expect(child.$el.innerHTML).toBe('<p>two</p>')
  136. })
  137. it('slot inside v-if', done => {
  138. const vm = new Vue({
  139. data: {
  140. a: 1,
  141. b: 2,
  142. show: true
  143. },
  144. template: '<test :show="show"><p slot="b">{{b}}</a><p>{{a}}</p></test>',
  145. components: {
  146. test: {
  147. props: ['show'],
  148. template: '<div v-if="show"><slot></slot><slot name="b"></slot></div>'
  149. }
  150. }
  151. }).$mount()
  152. expect(vm.$el.textContent).toBe('12')
  153. vm.a = 2
  154. waitForUpdate(() => {
  155. expect(vm.$el.textContent).toBe('22')
  156. vm.show = false
  157. }).then(() => {
  158. expect(vm.$el.textContent).toBe('')
  159. vm.show = true
  160. vm.a = 3
  161. }).then(() => {
  162. expect(vm.$el.textContent).toBe('32')
  163. }).then(done)
  164. })
  165. it('slot inside v-for', () => {
  166. mount({
  167. childTemplate: '<div><slot v-for="i in 3" :name="i"></slot></div>',
  168. parentContent: '<p v-for="i in 3" :slot="i">{{ i - 1 }}</p>'
  169. })
  170. expect(child.$el.innerHTML).toBe('<p>0</p><p>1</p><p>2</p>')
  171. })
  172. it('nested slots', done => {
  173. const vm = new Vue({
  174. template: '<test><test2><p>{{ msg }}</p></test2></test>',
  175. data: {
  176. msg: 'foo'
  177. },
  178. components: {
  179. test: {
  180. template: '<div><slot></slot></div>'
  181. },
  182. test2: {
  183. template: '<div><slot></slot></div>'
  184. }
  185. }
  186. }).$mount()
  187. expect(vm.$el.innerHTML).toBe('<div><p>foo</p></div>')
  188. vm.msg = 'bar'
  189. waitForUpdate(() => {
  190. expect(vm.$el.innerHTML).toBe('<div><p>bar</p></div>')
  191. }).then(done)
  192. })
  193. it('v-if on inserted content', done => {
  194. const vm = new Vue({
  195. template: '<test><p v-if="ok">{{ msg }}</p></test>',
  196. data: {
  197. ok: true,
  198. msg: 'hi'
  199. },
  200. components: {
  201. test: {
  202. template: '<div><slot>fallback</slot></div>'
  203. }
  204. }
  205. }).$mount()
  206. expect(vm.$el.innerHTML).toBe('<p>hi</p>')
  207. vm.ok = false
  208. waitForUpdate(() => {
  209. expect(vm.$el.innerHTML).toBe('fallback')
  210. vm.ok = true
  211. vm.msg = 'bye'
  212. }).then(() => {
  213. expect(vm.$el.innerHTML).toBe('<p>bye</p>')
  214. }).then(done)
  215. })
  216. it('template slot', function () {
  217. const vm = new Vue({
  218. template: '<test><template slot="test">hello</template></test>',
  219. components: {
  220. test: {
  221. template: '<div><slot name="test"></slot> world</div>'
  222. }
  223. }
  224. }).$mount()
  225. expect(vm.$el.innerHTML).toBe('hello world')
  226. })
  227. it('combined with v-for', () => {
  228. const vm = new Vue({
  229. template: '<div><test v-for="i in 3" :key="i">{{ i }}</test></div>',
  230. components: {
  231. test: {
  232. template: '<div><slot></slot></div>'
  233. }
  234. }
  235. }).$mount()
  236. expect(vm.$el.innerHTML).toBe('<div>1</div><div>2</div><div>3</div>')
  237. })
  238. it('inside template v-if', () => {
  239. mount({
  240. childTemplate: `
  241. <div>
  242. <template v-if="true"><slot></slot></template>
  243. </div>
  244. `,
  245. parentContent: 'foo'
  246. })
  247. expect(child.$el.innerHTML).toBe('foo')
  248. })
  249. it('default slot should use fallback content if has only whitespace', () => {
  250. Vue.config.preserveWhitespace = true
  251. mount({
  252. childTemplate: `
  253. <div>
  254. <slot name="first"><p>first slot</p></slot>
  255. <slot><p>this is the default slot</p></slot>
  256. <slot name="second"><p>second named slot</p></slot>
  257. </div>
  258. `,
  259. parentContent: `<div slot="first">1</div> <div slot="second">2</div>`
  260. })
  261. expect(child.$el.innerHTML).toBe(
  262. '<div>1</div> <p>this is the default slot</p> <div>2</div>'
  263. )
  264. Vue.config.preserveWhitespace = false
  265. })
  266. it('programmatic access to $slots', () => {
  267. const vm = new Vue({
  268. template: '<test><p slot="a">A</p><div>C</div><p slot="b">B</div></p></test>',
  269. components: {
  270. test: {
  271. render () {
  272. expect(this.$slots.a.length).toBe(1)
  273. expect(this.$slots.a[0].tag).toBe('p')
  274. expect(this.$slots.a[0].children.length).toBe(1)
  275. expect(this.$slots.a[0].children[0].text).toBe('A')
  276. expect(this.$slots.b.length).toBe(1)
  277. expect(this.$slots.b[0].tag).toBe('p')
  278. expect(this.$slots.b[0].children.length).toBe(1)
  279. expect(this.$slots.b[0].children[0].text).toBe('B')
  280. expect(this.$slots.default.length).toBe(1)
  281. expect(this.$slots.default[0].tag).toBe('div')
  282. expect(this.$slots.default[0].children.length).toBe(1)
  283. expect(this.$slots.default[0].children[0].text).toBe('C')
  284. return this.$slots.default[0]
  285. }
  286. }
  287. }
  288. }).$mount()
  289. expect(vm.$el.tagName).toBe('DIV')
  290. expect(vm.$el.textContent).toBe('C')
  291. })
  292. it('warn if user directly returns array', () => {
  293. new Vue({
  294. template: '<test><div></div></test>',
  295. components: {
  296. test: {
  297. render () {
  298. return this.$slots.default
  299. }
  300. }
  301. }
  302. }).$mount()
  303. expect('Render function should return a single root node').toHaveBeenWarned()
  304. })
  305. // #3254
  306. it('should not keep slot name when passed further down', () => {
  307. const vm = new Vue({
  308. template: '<test><span slot="foo">foo</span></test>',
  309. components: {
  310. test: {
  311. template: '<child><slot name="foo"></slot></child>',
  312. components: {
  313. child: {
  314. template: `
  315. <div>
  316. <div class="default"><slot></slot></div>
  317. <div class="named"><slot name="foo"></slot></div>
  318. </div>
  319. `
  320. }
  321. }
  322. }
  323. }
  324. }).$mount()
  325. expect(vm.$el.querySelector('.default').textContent).toBe('foo')
  326. expect(vm.$el.querySelector('.named').textContent).toBe('')
  327. })
  328. it('should not keep slot name when passed further down (nested)', () => {
  329. const vm = new Vue({
  330. template: '<wrap><test><span slot="foo">foo</span></test></wrap>',
  331. components: {
  332. wrap: {
  333. template: '<div><slot></slot></div>'
  334. },
  335. test: {
  336. template: '<child><slot name="foo"></slot></child>',
  337. components: {
  338. child: {
  339. template: `
  340. <div>
  341. <div class="default"><slot></slot></div>
  342. <div class="named"><slot name="foo"></slot></div>
  343. </div>
  344. `
  345. }
  346. }
  347. }
  348. }
  349. }).$mount()
  350. expect(vm.$el.querySelector('.default').textContent).toBe('foo')
  351. expect(vm.$el.querySelector('.named').textContent).toBe('')
  352. })
  353. it('should not keep slot name when passed further down (functional)', () => {
  354. const child = {
  355. template: `
  356. <div>
  357. <div class="default"><slot></slot></div>
  358. <div class="named"><slot name="foo"></slot></div>
  359. </div>
  360. `
  361. }
  362. const vm = new Vue({
  363. template: '<test><span slot="foo">foo</span></test>',
  364. components: {
  365. test: {
  366. functional: true,
  367. render (h, ctx) {
  368. const slots = ctx.slots()
  369. return h(child, slots.foo)
  370. }
  371. }
  372. }
  373. }).$mount()
  374. expect(vm.$el.querySelector('.default').textContent).toBe('foo')
  375. expect(vm.$el.querySelector('.named').textContent).toBe('')
  376. })
  377. // #3400
  378. it('named slots should be consistent across re-renders', done => {
  379. const vm = new Vue({
  380. template: `
  381. <comp>
  382. <div slot="foo">foo</div>
  383. </comp>
  384. `,
  385. components: {
  386. comp: {
  387. data () {
  388. return { a: 1 }
  389. },
  390. template: `<div><slot name="foo"></slot>{{ a }}</div>`
  391. }
  392. }
  393. }).$mount()
  394. expect(vm.$el.textContent).toBe('foo1')
  395. vm.$children[0].a = 2
  396. waitForUpdate(() => {
  397. expect(vm.$el.textContent).toBe('foo2')
  398. }).then(done)
  399. })
  400. // #3437
  401. it('should correctly re-create components in slot', done => {
  402. const calls = []
  403. const vm = new Vue({
  404. template: `
  405. <comp ref="child">
  406. <div slot="foo">
  407. <child></child>
  408. </div>
  409. </comp>
  410. `,
  411. components: {
  412. comp: {
  413. data () {
  414. return { ok: true }
  415. },
  416. template: `<div><slot name="foo" v-if="ok"></slot></div>`
  417. },
  418. child: {
  419. template: '<div>child</div>',
  420. created () {
  421. calls.push(1)
  422. },
  423. destroyed () {
  424. calls.push(2)
  425. }
  426. }
  427. }
  428. }).$mount()
  429. expect(calls).toEqual([1])
  430. vm.$refs.child.ok = false
  431. waitForUpdate(() => {
  432. expect(calls).toEqual([1, 2])
  433. vm.$refs.child.ok = true
  434. }).then(() => {
  435. expect(calls).toEqual([1, 2, 1])
  436. vm.$refs.child.ok = false
  437. }).then(() => {
  438. expect(calls).toEqual([1, 2, 1, 2])
  439. }).then(done)
  440. })
  441. it('warn duplicate slots', () => {
  442. new Vue({
  443. template: `<div>
  444. <test>
  445. <div>foo</div>
  446. <div slot="a">bar</div>
  447. </test>
  448. </div>`,
  449. components: {
  450. test: {
  451. template: `<div>
  452. <slot></slot><slot></slot>
  453. <div v-for="i in 3"><slot name="a"></slot></div>
  454. </div>`
  455. }
  456. }
  457. }).$mount()
  458. expect('Duplicate presence of slot "default"').toHaveBeenWarned()
  459. expect('Duplicate presence of slot "a"').toHaveBeenWarned()
  460. })
  461. it('should not warn valid conditional slots', () => {
  462. new Vue({
  463. template: `<div>
  464. <test>
  465. <div>foo</div>
  466. </test>
  467. </div>`,
  468. components: {
  469. test: {
  470. template: `<div>
  471. <slot v-if="true"></slot>
  472. <slot v-else></slot>
  473. </div>`
  474. }
  475. }
  476. }).$mount()
  477. expect('Duplicate presence of slot "default"').not.toHaveBeenWarned()
  478. })
  479. // #3518
  480. it('events should not break when slot is toggled by v-if', done => {
  481. const spy = jasmine.createSpy()
  482. const vm = new Vue({
  483. template: `<test><div class="click" @click="test">hi</div></test>`,
  484. methods: {
  485. test: spy
  486. },
  487. components: {
  488. test: {
  489. data: () => ({
  490. toggle: true
  491. }),
  492. template: `<div v-if="toggle"><slot></slot></div>`
  493. }
  494. }
  495. }).$mount()
  496. expect(vm.$el.textContent).toBe('hi')
  497. vm.$children[0].toggle = false
  498. waitForUpdate(() => {
  499. vm.$children[0].toggle = true
  500. }).then(() => {
  501. triggerEvent(vm.$el.querySelector('.click'), 'click')
  502. expect(spy).toHaveBeenCalled()
  503. }).then(done)
  504. })
  505. it('renders static tree with text', () => {
  506. const vm = new Vue({
  507. template: `<div><test><template><div></div>Hello<div></div></template></test></div>`,
  508. components: {
  509. test: {
  510. template: '<div><slot></slot></div>'
  511. }
  512. }
  513. })
  514. vm.$mount()
  515. expect('Error when rendering root').not.toHaveBeenWarned()
  516. })
  517. // #3872
  518. it('functional component as slot', () => {
  519. const vm = new Vue({
  520. template: `
  521. <parent>
  522. <child>one</child>
  523. <child slot="a">two</child>
  524. </parent>
  525. `,
  526. components: {
  527. parent: {
  528. template: `<div><slot name="a"></slot><slot></slot></div>`
  529. },
  530. child: {
  531. functional: true,
  532. render (h, { slots }) {
  533. return h('div', slots().default)
  534. }
  535. }
  536. }
  537. }).$mount()
  538. expect(vm.$el.innerHTML.trim()).toBe('<div>two</div><div>one</div>')
  539. })
  540. // #4209
  541. it('slot of multiple text nodes should not be infinitely merged', done => {
  542. const wrap = {
  543. template: `<inner ref="inner">foo<slot></slot></inner>`,
  544. components: {
  545. inner: {
  546. data: () => ({ a: 1 }),
  547. template: `<div>{{a}}<slot></slot></div>`
  548. }
  549. }
  550. }
  551. const vm = new Vue({
  552. template: `<wrap ref="wrap">bar</wrap>`,
  553. components: { wrap }
  554. }).$mount()
  555. expect(vm.$el.textContent).toBe('1foobar')
  556. vm.$refs.wrap.$refs.inner.a++
  557. waitForUpdate(() => {
  558. expect(vm.$el.textContent).toBe('2foobar')
  559. }).then(done)
  560. })
  561. // #4315
  562. it('functional component passing slot content to stateful child component', done => {
  563. const ComponentWithSlots = {
  564. render (h) {
  565. return h('div', this.$slots.slot1)
  566. }
  567. }
  568. const FunctionalComp = {
  569. functional: true,
  570. render (h) {
  571. return h(ComponentWithSlots, [h('span', { slot: 'slot1' }, 'foo')])
  572. }
  573. }
  574. const vm = new Vue({
  575. data: { n: 1 },
  576. render (h) {
  577. return h('div', [this.n, h(FunctionalComp)])
  578. }
  579. }).$mount()
  580. expect(vm.$el.textContent).toBe('1foo')
  581. vm.n++
  582. waitForUpdate(() => {
  583. // should not lose named slot
  584. expect(vm.$el.textContent).toBe('2foo')
  585. }).then(done)
  586. })
  587. })