apiCreateDynamicComponent.spec.ts 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. import { ref, shallowRef } from '@vue/reactivity'
  2. import {
  3. currentInstance,
  4. nextTick,
  5. resolveDynamicComponent,
  6. } from '@vue/runtime-dom'
  7. import { VaporDynamicComponentFlags } from '@vue/shared'
  8. import {
  9. type VaporComponentInstance,
  10. createComponent,
  11. createComponentWithFallback,
  12. createDynamicComponent,
  13. createSlot,
  14. defineVaporComponent,
  15. insert,
  16. renderEffect,
  17. setBlockKey,
  18. setHtml,
  19. setInsertionState,
  20. template,
  21. } from '../src'
  22. import { makeRender } from './_utils'
  23. const define = makeRender()
  24. describe('api: createDynamicComponent', () => {
  25. const A = () => document.createTextNode('AAA')
  26. const B = () => document.createTextNode('BBB')
  27. test('direct value', async () => {
  28. const val = shallowRef<any>(A)
  29. const { html } = define({
  30. setup() {
  31. return createDynamicComponent(() => val.value)
  32. },
  33. }).render()
  34. expect(html()).toBe('AAA<!--dynamic-component-->')
  35. val.value = B
  36. await nextTick()
  37. expect(html()).toBe('BBB<!--dynamic-component-->')
  38. // fallback
  39. val.value = 'foo'
  40. await nextTick()
  41. expect(html()).toBe('<foo></foo><!--dynamic-component-->')
  42. })
  43. test('global registration', async () => {
  44. const val = shallowRef('foo')
  45. const { app, html, mount } = define({
  46. setup() {
  47. return createDynamicComponent(() => val.value)
  48. },
  49. }).create()
  50. app.component('foo', A)
  51. app.component('bar', B)
  52. mount()
  53. expect(html()).toBe('AAA<!--dynamic-component-->')
  54. val.value = 'bar'
  55. await nextTick()
  56. expect(html()).toBe('BBB<!--dynamic-component-->')
  57. // fallback
  58. val.value = 'baz'
  59. await nextTick()
  60. expect(html()).toBe('<baz></baz><!--dynamic-component-->')
  61. })
  62. test('with v-once', async () => {
  63. const val = shallowRef<any>(A)
  64. const { html } = define({
  65. setup() {
  66. return createDynamicComponent(
  67. () => val.value,
  68. null,
  69. null,
  70. VaporDynamicComponentFlags.SINGLE_ROOT |
  71. VaporDynamicComponentFlags.ONCE,
  72. )
  73. },
  74. }).render()
  75. expect(html()).toBe('AAA<!--dynamic-component-->')
  76. val.value = B
  77. await nextTick()
  78. expect(html()).toBe('AAA<!--dynamic-component-->') // still AAA
  79. })
  80. test('fallback with v-once', async () => {
  81. const val = shallowRef<any>('button')
  82. const id = ref(0)
  83. const { html } = define({
  84. setup() {
  85. return createDynamicComponent(
  86. () => val.value,
  87. { id: () => id.value },
  88. null,
  89. VaporDynamicComponentFlags.SINGLE_ROOT |
  90. VaporDynamicComponentFlags.ONCE,
  91. )
  92. },
  93. }).render()
  94. expect(html()).toBe('<button id="0"></button><!--dynamic-component-->')
  95. id.value++
  96. await nextTick()
  97. expect(html()).toBe('<button id="0"></button><!--dynamic-component-->')
  98. })
  99. test('render fallback with insertionState', async () => {
  100. const { html, mount } = define({
  101. setup() {
  102. const html = ref('hi')
  103. const n1 = template('<div></div>', 1)() as any
  104. setInsertionState(n1)
  105. const n0 = createComponentWithFallback(
  106. resolveDynamicComponent('button') as any,
  107. ) as any
  108. renderEffect(() => setHtml(n0, html.value))
  109. return n1
  110. },
  111. }).create()
  112. mount()
  113. expect(html()).toBe('<div><button>hi</button></div>')
  114. })
  115. test('switch dynamic component children', async () => {
  116. const CompA = defineVaporComponent({
  117. setup() {
  118. return template('<div>A</div>')()
  119. },
  120. })
  121. const CompB = defineVaporComponent({
  122. setup() {
  123. return template('<div>B</div>')()
  124. },
  125. })
  126. const current = shallowRef(CompA)
  127. const { html } = define({
  128. setup() {
  129. const t1 = template('<div></div>')
  130. const n2 = t1() as any
  131. setInsertionState(n2)
  132. createDynamicComponent(() => current.value)
  133. return n2
  134. },
  135. }).render()
  136. expect(html()).toBe('<div><div>A</div><!--dynamic-component--></div>')
  137. current.value = CompB
  138. await nextTick()
  139. expect(html()).toBe('<div><div>B</div><!--dynamic-component--></div>')
  140. })
  141. test('fallback with dynamic slots', async () => {
  142. const slotName = ref('default')
  143. const { html } = define({
  144. setup() {
  145. return createDynamicComponent(() => 'div', null, {
  146. $: [
  147. () => ({
  148. name: slotName.value,
  149. fn: () => template('<span>hi</span>')(),
  150. }),
  151. ] as any,
  152. })
  153. },
  154. }).render()
  155. expect(html()).toBe(
  156. '<div><span>hi</span><!--slot--></div><!--dynamic-component-->',
  157. )
  158. // update slot name
  159. slotName.value = 'custom'
  160. await nextTick()
  161. expect(html()).toBe('<div><!--slot--></div><!--dynamic-component-->')
  162. slotName.value = 'default'
  163. await nextTick()
  164. expect(html()).toBe(
  165. '<div><span>hi</span><!--slot--></div><!--dynamic-component-->',
  166. )
  167. })
  168. test('fallback with function rawSlots as default slot', () => {
  169. const { html } = define({
  170. setup() {
  171. return createDynamicComponent(
  172. () => 'div',
  173. null,
  174. () => template('<span>hi</span>')(),
  175. )
  176. },
  177. }).render()
  178. expect(html()).toBe('<div><span>hi</span></div><!--dynamic-component-->')
  179. })
  180. test('reuses normalized function rawSlots on dynamic component updates', async () => {
  181. const rawSlots: unknown[] = []
  182. const CompA = defineVaporComponent({
  183. setup() {
  184. rawSlots.push((currentInstance as VaporComponentInstance).rawSlots)
  185. return template('<div>A</div>')()
  186. },
  187. })
  188. const CompB = defineVaporComponent({
  189. setup() {
  190. rawSlots.push((currentInstance as VaporComponentInstance).rawSlots)
  191. return template('<div>B</div>')()
  192. },
  193. })
  194. const current = shallowRef(CompA)
  195. const { html } = define({
  196. setup() {
  197. return createDynamicComponent(
  198. () => current.value,
  199. null,
  200. () => template('<span>slot</span>')(),
  201. )
  202. },
  203. }).render()
  204. expect(html()).toBe('<div>A</div><!--dynamic-component-->')
  205. current.value = CompB
  206. await nextTick()
  207. expect(html()).toBe('<div>B</div><!--dynamic-component-->')
  208. expect(rawSlots).toHaveLength(2)
  209. expect(rawSlots[1]).toBe(rawSlots[0])
  210. })
  211. test('compiled static key on dynamic component fallback', () => {
  212. const Comp = defineVaporComponent({
  213. setup() {
  214. const n0 = createDynamicComponent(
  215. () => 'div',
  216. null,
  217. null,
  218. VaporDynamicComponentFlags.SINGLE_ROOT,
  219. )
  220. setBlockKey(n0, 'foo')
  221. return n0
  222. },
  223. })
  224. const { html, instance } = define(Comp).render()
  225. expect(html()).toBe('<div></div><!--dynamic-component-->')
  226. const block = instance!.block as any
  227. expect(block.$key).toBe('foo')
  228. expect(block.nodes.$key).toBe('foo')
  229. })
  230. test('resolves slot owner local components after dynamic updates', async () => {
  231. const current = shallowRef('Foo')
  232. const Foo = defineVaporComponent({
  233. setup() {
  234. return template('<span>foo</span>')()
  235. },
  236. })
  237. const Child = defineVaporComponent({
  238. setup() {
  239. const n0 = template('<section></section>')()
  240. insert(createSlot('default'), n0 as ParentNode)
  241. return n0
  242. },
  243. })
  244. const { html } = define({
  245. components: { Foo },
  246. setup() {
  247. return createComponent(Child, null, {
  248. default: () => createDynamicComponent(() => current.value),
  249. })
  250. },
  251. }).render()
  252. expect(html()).toBe(
  253. '<section><span>foo</span><!--dynamic-component--><!--slot--></section>',
  254. )
  255. current.value = 'div'
  256. await nextTick()
  257. expect(html()).toBe(
  258. '<section><div></div><!--dynamic-component--><!--slot--></section>',
  259. )
  260. current.value = 'Foo'
  261. await nextTick()
  262. expect(html()).toBe(
  263. '<section><span>foo</span><!--dynamic-component--><!--slot--></section>',
  264. )
  265. })
  266. test('accept blocks', async () => {
  267. const { html } = define({
  268. setup() {
  269. const n0 = template('<a>router link</a>')()
  270. return createDynamicComponent(() => n0)
  271. },
  272. }).render()
  273. expect(html()).toBe('<a>router link</a><!--dynamic-component-->')
  274. })
  275. })