componentSlots.spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. // NOTE: This test is implemented based on the case of `runtime-core/__test__/componentSlots.spec.ts`.
  2. import {
  3. createComponent,
  4. createForSlots,
  5. createIf,
  6. createSlot,
  7. createVaporApp,
  8. defineVaporComponent,
  9. insert,
  10. prepend,
  11. renderEffect,
  12. setText,
  13. template,
  14. } from '../src'
  15. import { currentInstance, nextTick, ref } from '@vue/runtime-dom'
  16. import { makeRender } from './_utils'
  17. import type { DynamicSlot } from '../src/componentSlots'
  18. const define = makeRender<any>()
  19. function renderWithSlots(slots: any): any {
  20. let instance: any
  21. const Comp = defineVaporComponent({
  22. setup() {
  23. const t0 = template('<div></div>')
  24. const n0 = t0()
  25. instance = currentInstance
  26. return n0
  27. },
  28. })
  29. const { render } = define({
  30. render() {
  31. return createComponent(Comp, {}, slots)
  32. },
  33. })
  34. render()
  35. return instance
  36. }
  37. describe('component: slots', () => {
  38. test('initSlots: instance.slots should be set correctly', () => {
  39. const { slots } = renderWithSlots({
  40. default: () => template('<span></span>')(),
  41. })
  42. expect(slots.default()).toMatchObject(document.createElement('span'))
  43. })
  44. test('updateSlots: instance.slots should be updated correctly', async () => {
  45. const flag1 = ref(true)
  46. let instance: any
  47. const Child = () => {
  48. instance = currentInstance
  49. return template('child')()
  50. }
  51. const { render } = define({
  52. render() {
  53. return createComponent(
  54. Child,
  55. {},
  56. {
  57. $: [
  58. () =>
  59. flag1.value
  60. ? { name: 'one', fn: () => template('<span></span>')() }
  61. : { name: 'two', fn: () => template('<div></div>')() },
  62. ],
  63. },
  64. )
  65. },
  66. })
  67. render()
  68. expect(instance.slots).toHaveProperty('one')
  69. expect(instance.slots).not.toHaveProperty('two')
  70. flag1.value = false
  71. await nextTick()
  72. expect(instance.slots).not.toHaveProperty('one')
  73. expect(instance.slots).toHaveProperty('two')
  74. })
  75. test.todo('should work with createFlorSlots', async () => {
  76. const loop = ref([1, 2, 3])
  77. let instance: any
  78. const Child = () => {
  79. instance = currentInstance
  80. return template('child')()
  81. }
  82. const { render } = define({
  83. setup() {
  84. return createComponent(Child, null, {
  85. $: [
  86. () =>
  87. // @ts-expect-error
  88. createForSlots(loop.value, (item, i) => ({
  89. name: item,
  90. fn: () => template(item + i)(),
  91. })),
  92. ],
  93. })
  94. },
  95. })
  96. render()
  97. expect(instance.slots).toHaveProperty('1')
  98. expect(instance.slots).toHaveProperty('2')
  99. expect(instance.slots).toHaveProperty('3')
  100. loop.value.push(4)
  101. await nextTick()
  102. expect(instance.slots).toHaveProperty('4')
  103. loop.value.shift()
  104. await nextTick()
  105. expect(instance.slots).not.toHaveProperty('1')
  106. })
  107. // passes but no warning for slot invocation in vapor currently
  108. test.todo('should not warn when mounting another app in setup', () => {
  109. const Comp = defineVaporComponent({
  110. setup(_, { slots }) {
  111. return slots.default!()
  112. },
  113. })
  114. const mountComp = () => {
  115. createVaporApp({
  116. render() {
  117. return createComponent(
  118. Comp,
  119. {},
  120. { default: () => template('msg')() },
  121. )!
  122. },
  123. })
  124. }
  125. const App = {
  126. setup() {
  127. mountComp()
  128. return []
  129. },
  130. }
  131. createVaporApp(App).mount(document.createElement('div'))
  132. expect(
  133. 'Slot "default" invoked outside of the render function',
  134. ).not.toHaveBeenWarned()
  135. })
  136. describe('createSlot', () => {
  137. test('slot should be rendered correctly', () => {
  138. const Comp = defineVaporComponent(() => {
  139. const n0 = template('<div>')()
  140. insert(createSlot('header'), n0 as any as ParentNode)
  141. return n0
  142. })
  143. const { host } = define(() => {
  144. return createComponent(Comp, null, {
  145. header: () => template('header')(),
  146. })
  147. }).render()
  148. expect(host.innerHTML).toBe('<div>header<!--slot--></div>')
  149. })
  150. test('slot should be rendered correctly with slot props', async () => {
  151. const src = ref('header')
  152. const Comp = defineVaporComponent(() => {
  153. const n0 = template('<div></div>')()
  154. insert(
  155. createSlot('header', { title: () => src.value }),
  156. n0 as any as ParentNode,
  157. )
  158. return n0
  159. })
  160. const { host } = define(() => {
  161. return createComponent(Comp, null, {
  162. header: props => {
  163. const el = template('<h1></h1>')()
  164. renderEffect(() => {
  165. setText(el, props.title)
  166. })
  167. return el
  168. },
  169. })
  170. }).render()
  171. expect(host.innerHTML).toBe('<div><h1>header</h1><!--slot--></div>')
  172. src.value = 'footer'
  173. await nextTick()
  174. expect(host.innerHTML).toBe('<div><h1>footer</h1><!--slot--></div>')
  175. })
  176. test('dynamic slot props', async () => {
  177. let props: any
  178. const bindObj = ref<Record<string, any>>({ foo: 1, baz: 'qux' })
  179. const Comp = defineVaporComponent(() =>
  180. createSlot('default', { $: [() => bindObj.value] }),
  181. )
  182. define(() =>
  183. createComponent(Comp, null, {
  184. default: _props => ((props = _props), []),
  185. }),
  186. ).render()
  187. expect(props).toEqual({ foo: 1, baz: 'qux' })
  188. bindObj.value.foo = 2
  189. await nextTick()
  190. expect(props).toEqual({ foo: 2, baz: 'qux' })
  191. delete bindObj.value.baz
  192. await nextTick()
  193. expect(props).toEqual({ foo: 2 })
  194. })
  195. test('dynamic slot props with static slot props', async () => {
  196. let props: any
  197. const foo = ref(0)
  198. const bindObj = ref<Record<string, any>>({ foo: 100, baz: 'qux' })
  199. const Comp = defineVaporComponent(() =>
  200. createSlot('default', {
  201. foo: () => foo.value,
  202. $: [() => bindObj.value],
  203. }),
  204. )
  205. define(() =>
  206. createComponent(Comp, null, {
  207. default: _props => ((props = _props), []),
  208. }),
  209. ).render()
  210. expect(props).toEqual({ foo: 100, baz: 'qux' })
  211. foo.value = 2
  212. await nextTick()
  213. expect(props).toEqual({ foo: 100, baz: 'qux' })
  214. delete bindObj.value.foo
  215. await nextTick()
  216. expect(props).toEqual({ foo: 2, baz: 'qux' })
  217. })
  218. test('dynamic slot should be rendered correctly with slot props', async () => {
  219. const val = ref('header')
  220. const Comp = defineVaporComponent(() => {
  221. const n0 = template('<div></div>')()
  222. prepend(
  223. n0 as any as ParentNode,
  224. createSlot('header', { title: () => val.value }),
  225. )
  226. return n0
  227. })
  228. const { host } = define(() => {
  229. // dynamic slot
  230. return createComponent(Comp, null, {
  231. $: [
  232. () => ({
  233. name: 'header',
  234. fn: (props: any) => {
  235. const el = template('<h1></h1>')()
  236. renderEffect(() => {
  237. setText(el, props.title)
  238. })
  239. return el
  240. },
  241. }),
  242. ],
  243. })
  244. }).render()
  245. expect(host.innerHTML).toBe('<div><h1>header</h1><!--slot--></div>')
  246. val.value = 'footer'
  247. await nextTick()
  248. expect(host.innerHTML).toBe('<div><h1>footer</h1><!--slot--></div>')
  249. })
  250. test('dynamic slot outlet should be render correctly with slot props', async () => {
  251. const val = ref('header')
  252. const Comp = defineVaporComponent(() => {
  253. const n0 = template('<div></div>')()
  254. prepend(
  255. n0 as any as ParentNode,
  256. createSlot(
  257. () => val.value, // dynamic slot outlet name
  258. ),
  259. )
  260. return n0
  261. })
  262. const { host } = define(() => {
  263. return createComponent(Comp, null, {
  264. header: () => template('header')(),
  265. footer: () => template('footer')(),
  266. })
  267. }).render()
  268. expect(host.innerHTML).toBe('<div>header<!--slot--></div>')
  269. val.value = 'footer'
  270. await nextTick()
  271. expect(host.innerHTML).toBe('<div>footer<!--slot--></div>')
  272. })
  273. test('fallback should be render correctly', () => {
  274. const Comp = defineVaporComponent(() => {
  275. const n0 = template('<div></div>')()
  276. insert(
  277. createSlot('header', undefined, () => template('fallback')()),
  278. n0 as any as ParentNode,
  279. )
  280. return n0
  281. })
  282. const { host } = define(() => {
  283. return createComponent(Comp, {}, {})
  284. }).render()
  285. expect(host.innerHTML).toBe('<div>fallback<!--slot--></div>')
  286. })
  287. test('dynamic slot should be updated correctly', async () => {
  288. const flag1 = ref(true)
  289. const Child = defineVaporComponent(() => {
  290. const temp0 = template('<p></p>')
  291. const el0 = temp0()
  292. const el1 = temp0()
  293. const slot1 = createSlot('one', null, () => template('one fallback')())
  294. const slot2 = createSlot('two', null, () => template('two fallback')())
  295. insert(slot1, el0 as any as ParentNode)
  296. insert(slot2, el1 as any as ParentNode)
  297. return [el0, el1]
  298. })
  299. const { host } = define(() => {
  300. return createComponent(Child, null, {
  301. $: [
  302. () =>
  303. flag1.value
  304. ? { name: 'one', fn: () => template('one content')() }
  305. : { name: 'two', fn: () => template('two content')() },
  306. ],
  307. })
  308. }).render()
  309. expect(host.innerHTML).toBe(
  310. '<p>one content<!--slot--></p><p>two fallback<!--slot--></p>',
  311. )
  312. flag1.value = false
  313. await nextTick()
  314. expect(host.innerHTML).toBe(
  315. '<p>one fallback<!--slot--></p><p>two content<!--slot--></p>',
  316. )
  317. flag1.value = true
  318. await nextTick()
  319. expect(host.innerHTML).toBe(
  320. '<p>one content<!--slot--></p><p>two fallback<!--slot--></p>',
  321. )
  322. })
  323. test('dynamic slot outlet should be updated correctly', async () => {
  324. const slotOutletName = ref('one')
  325. const Child = defineVaporComponent(() => {
  326. const temp0 = template('<p>')
  327. const el0 = temp0()
  328. const slot1 = createSlot(
  329. () => slotOutletName.value,
  330. undefined,
  331. () => template('fallback')(),
  332. )
  333. insert(slot1, el0 as any as ParentNode)
  334. return el0
  335. })
  336. const { host } = define(() => {
  337. return createComponent(
  338. Child,
  339. {},
  340. {
  341. one: () => template('one content')(),
  342. two: () => template('two content')(),
  343. },
  344. )
  345. }).render()
  346. expect(host.innerHTML).toBe('<p>one content<!--slot--></p>')
  347. slotOutletName.value = 'two'
  348. await nextTick()
  349. expect(host.innerHTML).toBe('<p>two content<!--slot--></p>')
  350. slotOutletName.value = 'none'
  351. await nextTick()
  352. expect(host.innerHTML).toBe('<p>fallback<!--slot--></p>')
  353. })
  354. test('non-exist slot', async () => {
  355. const Child = defineVaporComponent(() => {
  356. const el0 = template('<p>')()
  357. const slot = createSlot('not-exist', undefined)
  358. insert(slot, el0 as any as ParentNode)
  359. return el0
  360. })
  361. const { host } = define(() => {
  362. return createComponent(Child)
  363. }).render()
  364. expect(host.innerHTML).toBe('<p><!--slot--></p>')
  365. })
  366. test('use fallback when inner content changes', async () => {
  367. const Child = {
  368. setup() {
  369. return createSlot('default', null, () =>
  370. document.createTextNode('fallback'),
  371. )
  372. },
  373. }
  374. const toggle = ref(true)
  375. const { html } = define({
  376. setup() {
  377. return createComponent(Child, null, {
  378. default: () => {
  379. return createIf(
  380. () => toggle.value,
  381. () => {
  382. return document.createTextNode('content')
  383. },
  384. )
  385. },
  386. })
  387. },
  388. }).render()
  389. expect(html()).toBe('content<!--if--><!--slot-->')
  390. toggle.value = false
  391. await nextTick()
  392. expect(html()).toBe('fallback<!--if--><!--slot-->')
  393. toggle.value = true
  394. await nextTick()
  395. expect(html()).toBe('content<!--if--><!--slot-->')
  396. })
  397. test('dynamic slot work with v-if', async () => {
  398. const val = ref('header')
  399. const toggle = ref(false)
  400. const Comp = defineVaporComponent(() => {
  401. const n0 = template('<div></div>')()
  402. prepend(n0 as any as ParentNode, createSlot('header', null))
  403. return n0
  404. })
  405. const { host } = define(() => {
  406. // dynamic slot
  407. return createComponent(Comp, null, {
  408. $: [
  409. () =>
  410. (toggle.value
  411. ? {
  412. name: val.value,
  413. fn: () => {
  414. return template('<h1></h1>')()
  415. },
  416. }
  417. : void 0) as DynamicSlot,
  418. ],
  419. })
  420. }).render()
  421. expect(host.innerHTML).toBe('<div><!--slot--></div>')
  422. toggle.value = true
  423. await nextTick()
  424. expect(host.innerHTML).toBe('<div><h1></h1><!--slot--></div>')
  425. })
  426. })
  427. })