for.spec.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. import Alpine from 'alpinejs'
  2. import { wait } from '@testing-library/dom'
  3. global.MutationObserver = class {
  4. observe() {}
  5. }
  6. test('x-for', async () => {
  7. document.body.innerHTML = `
  8. <div x-data="{ items: ['foo'] }">
  9. <button x-on:click="items = ['foo', 'bar']"></button>
  10. <template x-for="item in items">
  11. <span x-text="item"></span>
  12. </template>
  13. </div>
  14. `
  15. Alpine.start()
  16. expect(document.querySelectorAll('span').length).toEqual(1)
  17. expect(document.querySelectorAll('span')[0].textContent).toEqual('foo')
  18. document.querySelector('button').click()
  19. await wait(() => { expect(document.querySelectorAll('span').length).toEqual(2) })
  20. expect(document.querySelectorAll('span')[0].textContent).toEqual('foo')
  21. expect(document.querySelectorAll('span')[1].textContent).toEqual('bar')
  22. })
  23. test('removes all elements when array is empty and previously had one item', async () => {
  24. document.body.innerHTML = `
  25. <div x-data="{ items: ['foo'] }">
  26. <button x-on:click="items = []"></button>
  27. <template x-for="item in items">
  28. <span x-text="item"></span>
  29. </template>
  30. </div>
  31. `
  32. Alpine.start()
  33. expect(document.querySelectorAll('span').length).toEqual(1)
  34. document.querySelector('button').click()
  35. await wait(() => { expect(document.querySelectorAll('span').length).toEqual(0) })
  36. })
  37. test('removes all elements when array is empty and previously had multiple items', async () => {
  38. document.body.innerHTML = `
  39. <div x-data="{ items: ['foo', 'bar', 'world'] }">
  40. <button x-on:click="items = []"></button>
  41. <template x-for="item in items">
  42. <span x-text="item"></span>
  43. </template>
  44. </div>
  45. `
  46. Alpine.start()
  47. expect(document.querySelectorAll('span').length).toEqual(3)
  48. document.querySelector('button').click()
  49. await wait(() => { expect(document.querySelectorAll('span').length).toEqual(0) })
  50. })
  51. test('elements inside of loop are reactive', async () => {
  52. document.body.innerHTML = `
  53. <div x-data="{ items: ['first'], foo: 'bar' }">
  54. <button x-on:click="foo = 'baz'"></button>
  55. <template x-for="item in items">
  56. <span>
  57. <h1 x-text="item"></h1>
  58. <h2 x-text="foo"></h2>
  59. </span>
  60. </template>
  61. </div>
  62. `
  63. Alpine.start()
  64. expect(document.querySelectorAll('span').length).toEqual(1)
  65. expect(document.querySelector('h1').textContent).toEqual('first')
  66. expect(document.querySelector('h2').textContent).toEqual('bar')
  67. document.querySelector('button').click()
  68. await wait(() => {
  69. expect(document.querySelector('h1').textContent).toEqual('first')
  70. expect(document.querySelector('h2').textContent).toEqual('baz')
  71. })
  72. })
  73. test('components inside of loop are reactive', async () => {
  74. document.body.innerHTML = `
  75. <div x-data="{ items: ['first'] }">
  76. <template x-for="item in items">
  77. <div x-data="{foo: 'bar'}" class="child">
  78. <span x-text="foo"></span>
  79. <button x-on:click="foo = 'bob'"></button>
  80. </div>
  81. </template>
  82. </div>
  83. `
  84. Alpine.start()
  85. expect(document.querySelectorAll('div.child').length).toEqual(1)
  86. expect(document.querySelector('span').textContent).toEqual('bar')
  87. document.querySelector('button').click()
  88. await wait(() => {
  89. expect(document.querySelector('span').textContent).toEqual('bob')
  90. })
  91. })
  92. test('components inside a plain element of loop are reactive', async () => {
  93. document.body.innerHTML = `
  94. <div x-data="{ items: ['first'] }">
  95. <template x-for="item in items">
  96. <ul>
  97. <div x-data="{foo: 'bar'}" class="child">
  98. <span x-text="foo"></span>
  99. <button x-on:click="foo = 'bob'"></button>
  100. </div>
  101. </ul>
  102. </template>
  103. </div>
  104. `
  105. Alpine.start()
  106. expect(document.querySelectorAll('ul').length).toEqual(1)
  107. expect(document.querySelector('span').textContent).toEqual('bar')
  108. document.querySelector('button').click()
  109. await wait(() => {
  110. expect(document.querySelector('span').textContent).toEqual('bob')
  111. })
  112. })
  113. test('adding key attribute moves dom nodes properly', async () => {
  114. document.body.innerHTML = `
  115. <div x-data="{ items: ['foo', 'bar'] }">
  116. <button x-on:click="items = ['bar', 'foo', 'baz']"></button>
  117. <template x-for="item in items" :key="item">
  118. <span x-text="item"></span>
  119. </template>
  120. </div>
  121. `
  122. Alpine.start()
  123. expect(document.querySelectorAll('span').length).toEqual(2)
  124. const itemA = document.querySelectorAll('span')[0]
  125. itemA.setAttribute('order', 'first')
  126. const itemB = document.querySelectorAll('span')[1]
  127. itemB.setAttribute('order', 'second')
  128. document.querySelector('button').click()
  129. await wait(() => { expect(document.querySelectorAll('span').length).toEqual(3) })
  130. expect(document.querySelectorAll('span')[0].getAttribute('order')).toEqual('second')
  131. expect(document.querySelectorAll('span')[1].getAttribute('order')).toEqual('first')
  132. expect(document.querySelectorAll('span')[2].getAttribute('order')).toEqual(null)
  133. })
  134. test('can key by index', async () => {
  135. document.body.innerHTML = `
  136. <div x-data="{ items: ['foo', 'bar'] }">
  137. <button x-on:click="items = ['bar', 'foo', 'baz']"></button>
  138. <template x-for="(item, index) in items" :key="index">
  139. <span x-text="item"></span>
  140. </template>
  141. </div>
  142. `
  143. Alpine.start()
  144. expect(document.querySelectorAll('span').length).toEqual(2)
  145. document.querySelector('button').click()
  146. await wait(() => { expect(document.querySelectorAll('span').length).toEqual(3) })
  147. })
  148. test('can use index inside of loop', async () => {
  149. document.body.innerHTML = `
  150. <div x-data="{ items: ['foo'] }">
  151. <template x-for="(item, index) in items">
  152. <div>
  153. <h1 x-text="items.indexOf(item)"></h1>
  154. <h2 x-text="index"></h2>
  155. </div>
  156. </template>
  157. </div>
  158. `
  159. Alpine.start()
  160. expect(document.querySelector('h1').textContent).toEqual('0')
  161. expect(document.querySelector('h2').textContent).toEqual('0')
  162. })
  163. test('can use third iterator param (collection) inside of loop', async () => {
  164. document.body.innerHTML = `
  165. <div x-data="{ items: ['foo'] }">
  166. <template x-for="(item, index, things) in items">
  167. <div>
  168. <h1 x-text="items"></h1>
  169. <h2 x-text="things"></h2>
  170. </div>
  171. </template>
  172. </div>
  173. `
  174. Alpine.start()
  175. expect(document.querySelector('h1').textContent).toEqual('foo')
  176. expect(document.querySelector('h2').textContent).toEqual('foo')
  177. })
  178. test('can use x-if in conjunction with x-for', async () => {
  179. document.body.innerHTML = `
  180. <div x-data="{ items: ['foo', 'bar'], show: false }">
  181. <button @click="show = ! show"></button>
  182. <template x-if="show" x-for="item in items">
  183. <span x-text="item"></span>
  184. </template>
  185. </div>
  186. `
  187. Alpine.start()
  188. expect(document.querySelectorAll('span').length).toEqual(0)
  189. document.querySelector('button').click()
  190. await new Promise(resolve => setTimeout(resolve, 1))
  191. expect(document.querySelectorAll('span').length).toEqual(2)
  192. document.querySelector('button').click()
  193. await new Promise(resolve => setTimeout(resolve, 1))
  194. expect(document.querySelectorAll('span').length).toEqual(0)
  195. })
  196. test('listeners in loop get fresh iteration data even though they are only registered initially', async () => {
  197. document.body.innerHTML = `
  198. <div x-data="{ items: ['foo'], output: '' }">
  199. <button x-on:click="items = ['bar']"></button>
  200. <template x-for="(item, index) in items">
  201. <span x-text="item" x-on:click="output = item"></span>
  202. </template>
  203. <h1 x-text="output"></h1>
  204. </div>
  205. `
  206. Alpine.start()
  207. expect(document.querySelectorAll('span').length).toEqual(1)
  208. document.querySelector('span').click()
  209. await wait(() => { expect(document.querySelector('h1').textContent).toEqual('foo') })
  210. document.querySelector('button').click()
  211. await wait(() => { expect(document.querySelector('span').textContent).toEqual('bar') })
  212. document.querySelector('span').click()
  213. await wait(() => { expect(document.querySelector('h1').textContent).toEqual('bar') })
  214. })
  215. test('nested x-for', async () => {
  216. document.body.innerHTML = `
  217. <div x-data="{ foos: [ {bars: ['bob', 'lob']} ] }">
  218. <button x-on:click="foos = [ {bars: ['bob', 'lob']}, {bars: ['law']} ]"></button>
  219. <template x-for="foo in foos">
  220. <h1>
  221. <template x-for="bar in foo.bars">
  222. <h2 x-text="bar"></h2>
  223. </template>
  224. </h1>
  225. </template>
  226. </div>
  227. `
  228. Alpine.start()
  229. await wait(() => { expect(document.querySelectorAll('h1').length).toEqual(1) })
  230. await wait(() => { expect(document.querySelectorAll('h2').length).toEqual(2) })
  231. expect(document.querySelectorAll('h2')[0].textContent).toEqual('bob')
  232. expect(document.querySelectorAll('h2')[1].textContent).toEqual('lob')
  233. document.querySelector('button').click()
  234. await wait(() => { expect(document.querySelectorAll('h2').length).toEqual(3) })
  235. expect(document.querySelectorAll('h2')[0].textContent).toEqual('bob')
  236. expect(document.querySelectorAll('h2')[1].textContent).toEqual('lob')
  237. expect(document.querySelectorAll('h2')[2].textContent).toEqual('law')
  238. })
  239. test('x-for updates the right elements when new item are inserted at the beginning of the list', async () => {
  240. document.body.innerHTML = `
  241. <div x-data="{ items: [{name: 'one', key: '1'}, {name: 'two', key: '2'}] }">
  242. <button x-on:click="items = [{name: 'zero', key: '0'}, {name: 'one', key: '1'}, {name: 'two', key: '2'}]"></button>
  243. <template x-for="item in items" :key="item.key">
  244. <span x-text="item.name"></span>
  245. </template>
  246. </div>
  247. `
  248. Alpine.start()
  249. expect(document.querySelectorAll('span').length).toEqual(2)
  250. const itemA = document.querySelectorAll('span')[0]
  251. itemA.setAttribute('order', 'first')
  252. const itemB = document.querySelectorAll('span')[1]
  253. itemB.setAttribute('order', 'second')
  254. document.querySelector('button').click()
  255. await wait(() => { expect(document.querySelectorAll('span').length).toEqual(3) })
  256. expect(document.querySelectorAll('span')[0].textContent).toEqual('zero')
  257. expect(document.querySelectorAll('span')[1].textContent).toEqual('one')
  258. expect(document.querySelectorAll('span')[2].textContent).toEqual('two')
  259. // Make sure states are preserved
  260. expect(document.querySelectorAll('span')[0].getAttribute('order')).toEqual(null)
  261. expect(document.querySelectorAll('span')[1].getAttribute('order')).toEqual('first')
  262. expect(document.querySelectorAll('span')[2].getAttribute('order')).toEqual('second')
  263. })
  264. test('nested x-for access outer loop variable', async () => {
  265. document.body.innerHTML = `
  266. <div x-data="{ foos: [ {name: 'foo', bars: ['bob', 'lob']}, {name: 'baz', bars: ['bab', 'lab']} ] }">
  267. <template x-for="foo in foos">
  268. <h1>
  269. <template x-for="bar in foo.bars">
  270. <h2 x-text="foo.name+': '+bar"></h2>
  271. </template>
  272. </h1>
  273. </template>
  274. </div>
  275. `
  276. Alpine.start()
  277. await wait(() => { expect(document.querySelectorAll('h1').length).toEqual(2) })
  278. await wait(() => { expect(document.querySelectorAll('h2').length).toEqual(4) })
  279. expect(document.querySelectorAll('h2')[0].textContent).toEqual('foo: bob')
  280. expect(document.querySelectorAll('h2')[1].textContent).toEqual('foo: lob')
  281. expect(document.querySelectorAll('h2')[2].textContent).toEqual('baz: bab')
  282. expect(document.querySelectorAll('h2')[3].textContent).toEqual('baz: lab')
  283. })
  284. test('nested x-for event listeners', async () => {
  285. document._alerts = []
  286. document.body.innerHTML = `
  287. <div x-data="{ foos: [
  288. {name: 'foo', bars: [{name: 'bob', count: 0}, {name: 'lob', count: 0}]},
  289. {name: 'baz', bars: [{name: 'bab', count: 0}, {name: 'lab', count: 0}]}
  290. ], fnText: function(foo, bar) { return foo.name+': '+bar.name+' = '+bar.count; } }">
  291. <template x-for="foo in foos">
  292. <h1>
  293. <template x-for="bar in foo.bars">
  294. <h2 x-text="fnText(foo, bar)"
  295. x-on:click="bar.count += 1; document._alerts.push(fnText(foo, bar))"
  296. ></h2>
  297. </template>
  298. </h1>
  299. </template>
  300. </div>
  301. `
  302. Alpine.start()
  303. await wait(() => { expect(document.querySelectorAll('h1').length).toEqual(2) })
  304. await wait(() => { expect(document.querySelectorAll('h2').length).toEqual(4) })
  305. expect(document.querySelectorAll('h2')[0].textContent).toEqual('foo: bob = 0')
  306. expect(document.querySelectorAll('h2')[1].textContent).toEqual('foo: lob = 0')
  307. expect(document.querySelectorAll('h2')[2].textContent).toEqual('baz: bab = 0')
  308. expect(document.querySelectorAll('h2')[3].textContent).toEqual('baz: lab = 0')
  309. expect(document._alerts.length).toEqual(0)
  310. document.querySelectorAll('h2')[0].click()
  311. await wait(() => {
  312. expect(document.querySelectorAll('h2')[0].textContent).toEqual('foo: bob = 1')
  313. expect(document.querySelectorAll('h2')[1].textContent).toEqual('foo: lob = 0')
  314. expect(document.querySelectorAll('h2')[2].textContent).toEqual('baz: bab = 0')
  315. expect(document.querySelectorAll('h2')[3].textContent).toEqual('baz: lab = 0')
  316. expect(document._alerts.length).toEqual(1)
  317. expect(document._alerts[0]).toEqual('foo: bob = 1')
  318. })
  319. document.querySelectorAll('h2')[2].click()
  320. await wait(() => {
  321. expect(document.querySelectorAll('h2')[0].textContent).toEqual('foo: bob = 1')
  322. expect(document.querySelectorAll('h2')[1].textContent).toEqual('foo: lob = 0')
  323. expect(document.querySelectorAll('h2')[2].textContent).toEqual('baz: bab = 1')
  324. expect(document.querySelectorAll('h2')[3].textContent).toEqual('baz: lab = 0')
  325. expect(document._alerts.length).toEqual(2)
  326. expect(document._alerts[0]).toEqual('foo: bob = 1')
  327. expect(document._alerts[1]).toEqual('baz: bab = 1')
  328. })
  329. document.querySelectorAll('h2')[0].click()
  330. await wait(() => {
  331. expect(document.querySelectorAll('h2')[0].textContent).toEqual('foo: bob = 2')
  332. expect(document.querySelectorAll('h2')[1].textContent).toEqual('foo: lob = 0')
  333. expect(document.querySelectorAll('h2')[2].textContent).toEqual('baz: bab = 1')
  334. expect(document.querySelectorAll('h2')[3].textContent).toEqual('baz: lab = 0')
  335. expect(document._alerts.length).toEqual(3)
  336. expect(document._alerts[0]).toEqual('foo: bob = 1')
  337. expect(document._alerts[1]).toEqual('baz: bab = 1')
  338. expect(document._alerts[2]).toEqual('foo: bob = 2')
  339. })
  340. })
  341. test('make sure new elements with different keys added to the beginning of a loop are initialized instead of just updated', async () => {
  342. let clickCount = 0
  343. window.registerClick = () => {
  344. clickCount++
  345. }
  346. document.body.innerHTML = `
  347. <div x-data="{ items: ['foo'] }">
  348. <button @click="items = ['bar']">Change</button>
  349. <template x-for="item in items" :key="item">
  350. <h1 @click="registerClick()"></h1>
  351. </template>
  352. </div>
  353. `
  354. Alpine.start()
  355. document.querySelector('h1').click()
  356. expect(clickCount).toEqual(1)
  357. document.querySelector('button').click()
  358. document.querySelector('h1').click()
  359. expect(clickCount).toEqual(2)
  360. })
  361. test('x-for over range using i in x syntax', async () => {
  362. document.body.innerHTML = `
  363. <div x-data>
  364. <template x-for="i in 10">
  365. <span x-text="i"></span>
  366. </template>
  367. </div>
  368. `
  369. Alpine.start()
  370. expect(document.querySelectorAll('span').length).toEqual(10)
  371. })
  372. test('x-for over range using i in x syntax with data property', async () => {
  373. document.body.innerHTML = `
  374. <div x-data="{ count: 10 }">
  375. <template x-for="i in count">
  376. <span x-text="i"></span>
  377. </template>
  378. </div>
  379. `
  380. Alpine.start()
  381. expect(document.querySelectorAll('span').length).toEqual(10)
  382. })
  383. test('x-for with an array of numbers', async () => {
  384. document.body.innerHTML = `
  385. <div x-data="{ items: [] }">
  386. <template x-for="i in items">
  387. <span x-text="i"></span>
  388. </template>
  389. <button id="push-2" @click="items.push(2)"></button>
  390. <button id="push-3" @click="items.push(3)"></button>
  391. </div>
  392. `
  393. Alpine.start()
  394. document.querySelector('#push-2').click()
  395. await wait(() => {
  396. expect(document.querySelector('span').textContent).toEqual('2')
  397. })
  398. document.querySelector('#push-3').click()
  399. await wait(() => {
  400. expect(document.querySelectorAll('span').length).toEqual(2)
  401. expect(document.querySelectorAll('span')[1].textContent).toEqual('3')
  402. })
  403. })