1
0

x-for.spec.js 15 KB

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