component-slot.spec.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  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 content', 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('fallback content', () => {
  43. mount({
  44. childTemplate: '<div><slot><p>{{msg}}</p></slot></div>'
  45. })
  46. expect(child.$el.children[0].tagName).toBe('P')
  47. expect(child.$el.textContent).toBe('child message')
  48. })
  49. it('fallback content with multiple named slots', () => {
  50. mount({
  51. childTemplate: `
  52. <div>
  53. <slot name="a"><p>fallback a</p></slot>
  54. <slot name="b">fallback b</slot>
  55. </div>
  56. `,
  57. parentContent: '<p slot="b">slot b</p>'
  58. })
  59. expect(child.$el.children.length).toBe(2)
  60. expect(child.$el.children[0].textContent).toBe('fallback a')
  61. expect(child.$el.children[1].textContent).toBe('slot b')
  62. })
  63. it('fallback content with mixed named/unamed slots', () => {
  64. mount({
  65. childTemplate: `
  66. <div>
  67. <slot><p>fallback a</p></slot>
  68. <slot name="b">fallback b</slot>
  69. </div>
  70. `,
  71. parentContent: '<p slot="b">slot b</p>'
  72. })
  73. expect(child.$el.children.length).toBe(2)
  74. expect(child.$el.children[0].textContent).toBe('fallback a')
  75. expect(child.$el.children[1].textContent).toBe('slot b')
  76. })
  77. it('selector matching multiple elements', () => {
  78. mount({
  79. childTemplate: '<div><slot name="t"></slot></div>',
  80. parentContent: '<p slot="t">1</p><div></div><p slot="t">2</p>'
  81. })
  82. expect(child.$el.innerHTML).toBe('<p>1</p><p>2</p>')
  83. })
  84. it('default content should only render parts not selected', () => {
  85. mount({
  86. childTemplate: `
  87. <div>
  88. <slot name="a"></slot>
  89. <slot></slot>
  90. <slot name="b"></slot>
  91. </div>
  92. `,
  93. parentContent: '<div>foo</div><p slot="a">1</p><p slot="b">2</p>'
  94. })
  95. expect(child.$el.innerHTML).toBe('<p>1</p> <div>foo</div> <p>2</p>')
  96. })
  97. it('name should only match children', function () {
  98. mount({
  99. childTemplate: `
  100. <div>
  101. <slot name="a"><p>fallback a</p></slot>
  102. <slot name="b"><p>fallback b</p></slot>
  103. <slot name="c"><p>fallback c</p></slot>
  104. </div>
  105. `,
  106. parentContent: `
  107. '<p slot="b">select b</p>
  108. '<span><p slot="b">nested b</p></span>
  109. '<span><p slot="c">nested c</p></span>
  110. `
  111. })
  112. expect(child.$el.children.length).toBe(3)
  113. expect(child.$el.children[0].textContent).toBe('fallback a')
  114. expect(child.$el.children[1].textContent).toBe('select b')
  115. expect(child.$el.children[2].textContent).toBe('fallback c')
  116. })
  117. it('should accept expressions in slot attribute and slot names', () => {
  118. mount({
  119. childTemplate: `<div><slot :name="'a'"></slot></div>`,
  120. parentContent: `<p>one</p><p :slot="'a'">two</p>`
  121. })
  122. expect(child.$el.innerHTML).toBe('<p>two</p>')
  123. })
  124. it('slot inside v-if', done => {
  125. const vm = new Vue({
  126. data: {
  127. a: 1,
  128. b: 2,
  129. show: true
  130. },
  131. template: '<test :show="show"><p slot="b">{{b}}</a><p>{{a}}</p></test>',
  132. components: {
  133. test: {
  134. props: ['show'],
  135. template: '<div v-if="show"><slot></slot><slot name="b"></slot></div>'
  136. }
  137. }
  138. }).$mount()
  139. expect(vm.$el.textContent).toBe('12')
  140. vm.a = 2
  141. waitForUpdate(() => {
  142. expect(vm.$el.textContent).toBe('22')
  143. vm.show = false
  144. }).then(() => {
  145. expect(vm.$el.textContent).toBe('')
  146. vm.show = true
  147. vm.a = 3
  148. }).then(() => {
  149. expect(vm.$el.textContent).toBe('32')
  150. }).then(done)
  151. })
  152. it('slot inside v-for', () => {
  153. mount({
  154. childTemplate: '<div><slot v-for="i in 3" :name="i"></slot></div>',
  155. parentContent: '<p v-for="i in 3" :slot="i">{{ i - 1 }}</p>'
  156. })
  157. expect(child.$el.innerHTML).toBe('<p>0</p><p>1</p><p>2</p>')
  158. })
  159. it('nested slots', done => {
  160. const vm = new Vue({
  161. template: '<test><test2><p>{{ msg }}</p></test2></test>',
  162. data: {
  163. msg: 'foo'
  164. },
  165. components: {
  166. test: {
  167. template: '<div><slot></slot></div>'
  168. },
  169. test2: {
  170. template: '<div><slot></slot></div>'
  171. }
  172. }
  173. }).$mount()
  174. expect(vm.$el.innerHTML).toBe('<div><p>foo</p></div>')
  175. vm.msg = 'bar'
  176. waitForUpdate(() => {
  177. expect(vm.$el.innerHTML).toBe('<div><p>bar</p></div>')
  178. }).then(done)
  179. })
  180. it('v-if on inserted content', done => {
  181. const vm = new Vue({
  182. template: '<test><p v-if="ok">{{ msg }}</p></test>',
  183. data: {
  184. ok: true,
  185. msg: 'hi'
  186. },
  187. components: {
  188. test: {
  189. template: '<div><slot>fallback</slot></div>'
  190. }
  191. }
  192. }).$mount()
  193. expect(vm.$el.innerHTML).toBe('<p>hi</p>')
  194. vm.ok = false
  195. waitForUpdate(() => {
  196. expect(vm.$el.innerHTML).toBe('fallback')
  197. vm.ok = true
  198. vm.msg = 'bye'
  199. }).then(() => {
  200. expect(vm.$el.innerHTML).toBe('<p>bye</p>')
  201. }).then(done)
  202. })
  203. it('template slot', function () {
  204. const vm = new Vue({
  205. template: '<test><template slot="test">hello</template></test>',
  206. components: {
  207. test: {
  208. template: '<div><slot name="test"></slot> world</div>'
  209. }
  210. }
  211. }).$mount()
  212. expect(vm.$el.innerHTML).toBe('hello world')
  213. })
  214. it('combined with v-for', () => {
  215. const vm = new Vue({
  216. template: '<div><test v-for="i in 3">{{ i }}</test></div>',
  217. components: {
  218. test: {
  219. template: '<div><slot></slot></div>'
  220. }
  221. }
  222. }).$mount()
  223. expect(vm.$el.innerHTML).toBe('<div>1</div><div>2</div><div>3</div>')
  224. })
  225. it('inside template v-if', () => {
  226. mount({
  227. childTemplate: `
  228. <div>
  229. <template v-if="true"><slot></slot></template>
  230. </div>
  231. `,
  232. parentContent: 'foo'
  233. })
  234. expect(child.$el.innerHTML).toBe('foo')
  235. })
  236. it('default slot should use fallback content if has only whitespace', () => {
  237. Vue.config.preserveWhitespace = true
  238. mount({
  239. childTemplate: `
  240. <div>
  241. <slot name="first"><p>first slot</p></slot>
  242. <slot><p>this is the default slot</p></slot>
  243. <slot name="second"><p>second named slot</p></slot>
  244. </div>
  245. `,
  246. parentContent: `<div slot="first">1</div> <div slot="second">2</div>`
  247. })
  248. expect(child.$el.innerHTML).toBe(
  249. '<div>1</div> <p>this is the default slot</p> <div>2</div>'
  250. )
  251. Vue.config.preserveWhitespace = false
  252. })
  253. it('programmatic access to $slots', () => {
  254. const vm = new Vue({
  255. template: '<test><p slot="a">A</p><div>C</div><p slot="b">B</div></p></test>',
  256. components: {
  257. test: {
  258. render () {
  259. expect(this.$slots.a.length).toBe(1)
  260. expect(this.$slots.a[0].tag).toBe('p')
  261. expect(this.$slots.a[0].children.length).toBe(1)
  262. expect(this.$slots.a[0].children[0].text).toBe('A')
  263. expect(this.$slots.b.length).toBe(1)
  264. expect(this.$slots.b[0].tag).toBe('p')
  265. expect(this.$slots.b[0].children.length).toBe(1)
  266. expect(this.$slots.b[0].children[0].text).toBe('B')
  267. expect(this.$slots.default.length).toBe(1)
  268. expect(this.$slots.default[0].tag).toBe('div')
  269. expect(this.$slots.default[0].children.length).toBe(1)
  270. expect(this.$slots.default[0].children[0].text).toBe('C')
  271. return this.$slots.default[0]
  272. }
  273. }
  274. }
  275. }).$mount()
  276. expect(vm.$el.tagName).toBe('DIV')
  277. expect(vm.$el.textContent).toBe('C')
  278. })
  279. it('warn if user directly returns array', () => {
  280. new Vue({
  281. template: '<test><div></div></test>',
  282. components: {
  283. test: {
  284. render () {
  285. return this.$slots.default
  286. }
  287. }
  288. }
  289. }).$mount()
  290. expect('Render function should return a single root node').toHaveBeenWarned()
  291. })
  292. // #3254
  293. it('should not keep slot name when passed further down', () => {
  294. const vm = new Vue({
  295. template: '<test><span slot="foo">foo<span></test>',
  296. components: {
  297. test: {
  298. template: '<child><slot name="foo"></slot></child>',
  299. components: {
  300. child: {
  301. template: `
  302. <div>
  303. <div class="default"><slot></slot></div>
  304. <div class="named"><slot name="foo"></slot></div>
  305. </div>
  306. `
  307. }
  308. }
  309. }
  310. }
  311. }).$mount()
  312. expect(vm.$el.querySelector('.default').textContent).toBe('foo')
  313. expect(vm.$el.querySelector('.named').textContent).toBe('')
  314. })
  315. it('should not keep slot name when passed further down (nested)', () => {
  316. const vm = new Vue({
  317. template: '<wrap><test><span slot="foo">foo<span></test></wrap>',
  318. components: {
  319. wrap: {
  320. template: '<div><slot></slot></div>'
  321. },
  322. test: {
  323. template: '<child><slot name="foo"></slot></child>',
  324. components: {
  325. child: {
  326. template: `
  327. <div>
  328. <div class="default"><slot></slot></div>
  329. <div class="named"><slot name="foo"></slot></div>
  330. </div>
  331. `
  332. }
  333. }
  334. }
  335. }
  336. }).$mount()
  337. expect(vm.$el.querySelector('.default').textContent).toBe('foo')
  338. expect(vm.$el.querySelector('.named').textContent).toBe('')
  339. })
  340. it('should not keep slot name when passed further down (functional)', () => {
  341. const child = {
  342. template: `
  343. <div>
  344. <div class="default"><slot></slot></div>
  345. <div class="named"><slot name="foo"></slot></div>
  346. </div>
  347. `
  348. }
  349. const vm = new Vue({
  350. template: '<test><span slot="foo">foo<span></test>',
  351. components: {
  352. test: {
  353. functional: true,
  354. render (h, ctx) {
  355. const slots = ctx.slots()
  356. return h(child, slots.foo)
  357. }
  358. }
  359. }
  360. }).$mount()
  361. expect(vm.$el.querySelector('.default').textContent).toBe('foo')
  362. expect(vm.$el.querySelector('.named').textContent).toBe('')
  363. })
  364. // #3400
  365. it('named slots should be consistent across re-renders', done => {
  366. const vm = new Vue({
  367. template: `
  368. <comp>
  369. <div slot="foo">foo</div>
  370. </comp>
  371. `,
  372. components: {
  373. comp: {
  374. data () {
  375. return { a: 1 }
  376. },
  377. template: `<div><slot name="foo"></slot>{{ a }}</div>`
  378. }
  379. }
  380. }).$mount()
  381. expect(vm.$el.textContent).toBe('foo1')
  382. vm.$children[0].a = 2
  383. waitForUpdate(() => {
  384. expect(vm.$el.textContent).toBe('foo2')
  385. }).then(done)
  386. })
  387. // #3437
  388. it('should correctly re-create components in slot', done => {
  389. const calls = []
  390. const vm = new Vue({
  391. template: `
  392. <comp ref="child">
  393. <div slot="foo">
  394. <child></child>
  395. </div>
  396. </comp>
  397. `,
  398. components: {
  399. comp: {
  400. data () {
  401. return { ok: true }
  402. },
  403. template: `<div><slot name="foo" v-if="ok"></slot></div>`
  404. },
  405. child: {
  406. template: '<div>child</div>',
  407. created () {
  408. calls.push(1)
  409. },
  410. destroyed () {
  411. calls.push(2)
  412. }
  413. }
  414. }
  415. }).$mount()
  416. expect(calls).toEqual([1])
  417. vm.$refs.child.ok = false
  418. waitForUpdate(() => {
  419. expect(calls).toEqual([1, 2])
  420. vm.$refs.child.ok = true
  421. }).then(() => {
  422. expect(calls).toEqual([1, 2, 1])
  423. vm.$refs.child.ok = false
  424. }).then(() => {
  425. expect(calls).toEqual([1, 2, 1, 2])
  426. }).then(done)
  427. })
  428. it('warn duplicate slots', () => {
  429. new Vue({
  430. template: `<div>
  431. <test>
  432. <div>foo</div>
  433. <div slot="a">bar</div>
  434. </test>
  435. </div>`,
  436. components: {
  437. test: {
  438. template: `<div>
  439. <slot></slot><slot></slot>
  440. <div v-for="i in 3"><slot name="a"></slot></div>
  441. </div>`
  442. }
  443. }
  444. }).$mount()
  445. expect('Duplicate presence of slot "default"').toHaveBeenWarned()
  446. expect('Duplicate presence of slot "a"').toHaveBeenWarned()
  447. })
  448. it('should not warn valid conditional slots', () => {
  449. new Vue({
  450. template: `<div>
  451. <test>
  452. <div>foo</div>
  453. </test>
  454. </div>`,
  455. components: {
  456. test: {
  457. template: `<div>
  458. <slot v-if="true"></slot>
  459. <slot v-else></slot>
  460. </div>`
  461. }
  462. }
  463. }).$mount()
  464. expect('Duplicate presence of slot "default"').not.toHaveBeenWarned()
  465. })
  466. // #3518
  467. it('events should not break when slot is toggled by v-if', done => {
  468. const spy = jasmine.createSpy()
  469. const vm = new Vue({
  470. template: `<test><div class="click" @click="test">hi</div></test>`,
  471. methods: {
  472. test: spy
  473. },
  474. components: {
  475. test: {
  476. data: () => ({
  477. toggle: true
  478. }),
  479. template: `<div v-if="toggle"><slot></slot></div>`
  480. }
  481. }
  482. }).$mount()
  483. expect(vm.$el.textContent).toBe('hi')
  484. vm.$children[0].toggle = false
  485. waitForUpdate(() => {
  486. vm.$children[0].toggle = true
  487. }).then(() => {
  488. triggerEvent(vm.$el.querySelector('.click'), 'click')
  489. expect(spy).toHaveBeenCalled()
  490. }).then(done)
  491. })
  492. })