x-for.spec.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  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('removes all elements when array is empty and previously had one item',
  74. html`
  75. <div x-data="{ items: ['foo'] }">
  76. <button x-on:click="items = []">click me</button>
  77. <template x-for="item in items">
  78. <span x-text="item"></span>
  79. </template>
  80. </div>
  81. `,
  82. ({ get }) => {
  83. get('span').should(beVisible())
  84. get('button').click()
  85. get('span').should(notBeVisible())
  86. }
  87. )
  88. test('removes all elements when array is empty and previously had multiple items',
  89. html`
  90. <div x-data="{ items: ['foo', 'bar', 'world'] }">
  91. <button x-on:click="items = []">click me</button>
  92. <template x-for="item in items">
  93. <span x-text="item"></span>
  94. </template>
  95. </div>
  96. `,
  97. ({ get }) => {
  98. get('span:nth-of-type(1)').should(beVisible())
  99. get('span:nth-of-type(2)').should(beVisible())
  100. get('span:nth-of-type(3)').should(beVisible())
  101. get('button').click()
  102. get('span:nth-of-type(1)').should(notBeVisible())
  103. get('span:nth-of-type(2)').should(notBeVisible())
  104. get('span:nth-of-type(3)').should(notBeVisible())
  105. }
  106. )
  107. test('elements inside of loop are reactive',
  108. html`
  109. <div x-data="{ items: ['first'], foo: 'bar' }">
  110. <button x-on:click="foo = 'baz'">click me</button>
  111. <template x-for="item in items">
  112. <span>
  113. <h1 x-text="item"></h1>
  114. <h2 x-text="foo"></h2>
  115. </span>
  116. </template>
  117. </div>
  118. `,
  119. ({ get }) => {
  120. get('span').should(beVisible())
  121. get('h1').should(haveText('first'))
  122. get('h2').should(haveText('bar'))
  123. get('button').click()
  124. get('span').should(beVisible())
  125. get('h1').should(haveText('first'))
  126. get('h2').should(haveText('baz'))
  127. }
  128. )
  129. test('components inside of loop are reactive',
  130. html`
  131. <div x-data="{ items: ['first'] }">
  132. <template x-for="item in items">
  133. <div x-data="{foo: 'bar'}" class="child">
  134. <span x-text="foo"></span>
  135. <button x-on:click="foo = 'bob'">click me</button>
  136. </div>
  137. </template>
  138. </div>
  139. `,
  140. ({ get }) => {
  141. get('span').should(haveText('bar'))
  142. get('button').click()
  143. get('span').should(haveText('bob'))
  144. }
  145. )
  146. test('components inside a plain element of loop are reactive',
  147. html`
  148. <div x-data="{ items: ['first'] }">
  149. <template x-for="item in items">
  150. <ul>
  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. </ul>
  156. </template>
  157. </div>
  158. `,
  159. ({ get }) => {
  160. get('span').should(haveText('bar'))
  161. get('button').click()
  162. get('span').should(haveText('bob'))
  163. }
  164. )
  165. test('adding key attribute moves dom nodes properly',
  166. html`
  167. <div x-data="{ items: ['foo', 'bar'] }">
  168. <button x-on:click="items = ['bob', 'bar', 'foo', 'baz']" id="reorder">click me</button>
  169. <button x-on:click="$el.parentElement.querySelectorAll('span').forEach((el, index) => el.og_loop_index = index)" id="assign">click me</button>
  170. <template x-for="item in items" :key="item">
  171. <span x-text="item"></span>
  172. </template>
  173. </div>
  174. `,
  175. ({ get }) => {
  176. let haveOgIndex = index => el => expect(el[0].og_loop_index).to.equal(index)
  177. get('#assign').click()
  178. get('span:nth-of-type(1)').should(haveOgIndex(0))
  179. get('span:nth-of-type(2)').should(haveOgIndex(1))
  180. get('#reorder').click()
  181. get('span:nth-of-type(1)').should(haveOgIndex(undefined))
  182. get('span:nth-of-type(2)').should(haveOgIndex(1))
  183. get('span:nth-of-type(3)').should(haveOgIndex(0))
  184. get('span:nth-of-type(4)').should(haveOgIndex(undefined))
  185. }
  186. )
  187. test('can key by index',
  188. html`
  189. <div x-data="{ items: ['foo', 'bar'] }">
  190. <button x-on:click="items = ['bar', 'foo', 'baz']" id="reorder">click me</button>
  191. <button x-on:click="$el.parentElement.querySelectorAll('span').forEach((el, index) => el.og_loop_index = index)" id="assign">click me</button>
  192. <template x-for="(item, index) in items" :key="index">
  193. <span x-text="item"></span>
  194. </template>
  195. </div>
  196. `,
  197. ({ get }) => {
  198. let haveOgIndex = index => el => expect(el[0].og_loop_index).to.equal(index)
  199. get('#assign').click()
  200. get('span:nth-of-type(1)').should(haveOgIndex(0))
  201. get('span:nth-of-type(2)').should(haveOgIndex(1))
  202. get('#reorder').click()
  203. get('span:nth-of-type(1)').should(haveOgIndex(0))
  204. get('span:nth-of-type(2)').should(haveOgIndex(1))
  205. get('span:nth-of-type(3)').should(haveOgIndex(undefined))
  206. }
  207. )
  208. test('can use index inside of loop',
  209. html`
  210. <div x-data="{ items: ['foo'] }">
  211. <template x-for="(item, index) in items">
  212. <div>
  213. <h1 x-text="items.indexOf(item)"></h1>
  214. <h2 x-text="index"></h2>
  215. </div>
  216. </template>
  217. </div>
  218. `,
  219. ({ get }) => {
  220. get('h1').should(haveText(0))
  221. get('h2').should(haveText(0))
  222. }
  223. )
  224. test('can use third iterator param (collection) inside of loop',
  225. html`
  226. <div x-data="{ items: ['foo'] }">
  227. <template x-for="(item, index, things) in items">
  228. <div>
  229. <h1 x-text="items"></h1>
  230. <h2 x-text="things"></h2>
  231. </div>
  232. </template>
  233. </div>
  234. `,
  235. ({ get }) => {
  236. get('h1').should(haveText('foo'))
  237. get('h2').should(haveText('foo'))
  238. }
  239. )
  240. test('listeners in loop get fresh iteration data even though they are only registered initially',
  241. html`
  242. <div x-data="{ items: ['foo'], output: '' }">
  243. <button x-on:click="items = ['bar']">click me</button>
  244. <template x-for="(item, index) in items">
  245. <span x-text="item" x-on:click="output = item"></span>
  246. </template>
  247. <h1 x-text="output"></h1>
  248. </div>
  249. `,
  250. ({ get }) => {
  251. get('h1').should(haveText(''))
  252. get('span').click()
  253. get('h1').should(haveText('foo'))
  254. get('button').click()
  255. get('span').click()
  256. get('h1').should(haveText('bar'))
  257. }
  258. )
  259. test('nested x-for',
  260. html`
  261. <div x-data="{ foos: [ {bars: ['bob', 'lob']} ] }">
  262. <button x-on:click="foos = [ {bars: ['bob', 'lob']}, {bars: ['law']} ]">click me</button>
  263. <template x-for="foo in foos">
  264. <h1>
  265. <template x-for="bar in foo.bars">
  266. <h2 x-text="bar"></h2>
  267. </template>
  268. </h1>
  269. </template>
  270. </div>
  271. `,
  272. ({ get }) => {
  273. get('h1:nth-of-type(1) h2:nth-of-type(1)').should(beVisible())
  274. get('h1:nth-of-type(1) h2:nth-of-type(2)').should(beVisible())
  275. get('h1:nth-of-type(2) h2:nth-of-type(1)').should(notBeVisible())
  276. get('button').click()
  277. get('h1:nth-of-type(1) h2:nth-of-type(1)').should(beVisible())
  278. get('h1:nth-of-type(1) h2:nth-of-type(2)').should(beVisible())
  279. get('h1:nth-of-type(2) h2:nth-of-type(1)').should(beVisible())
  280. }
  281. )
  282. test('x-for updates the right elements when new item are inserted at the beginning of the list',
  283. html`
  284. <div x-data="{ items: [{name: 'one', key: '1'}, {name: 'two', key: '2'}] }">
  285. <button x-on:click="items = [{name: 'zero', key: '0'}, {name: 'one', key: '1'}, {name: 'two', key: '2'}]">click me</button>
  286. <template x-for="item in items" :key="item.key">
  287. <span x-text="item.name"></span>
  288. </template>
  289. </div>
  290. `,
  291. ({ get }) => {
  292. get('span:nth-of-type(1)').should(haveText('one'))
  293. get('span:nth-of-type(2)').should(haveText('two'))
  294. get('button').click()
  295. get('span:nth-of-type(1)').should(haveText('zero'))
  296. get('span:nth-of-type(2)').should(haveText('one'))
  297. get('span:nth-of-type(3)').should(haveText('two'))
  298. }
  299. )
  300. test('nested x-for access outer loop variable',
  301. html`
  302. <div x-data="{ foos: [ {name: 'foo', bars: ['bob', 'lob']}, {name: 'baz', bars: ['bab', 'lab']} ] }">
  303. <template x-for="foo in foos">
  304. <h1>
  305. <template x-for="bar in foo.bars">
  306. <h2 x-text="foo.name+': '+bar"></h2>
  307. </template>
  308. </h1>
  309. </template>
  310. </div>
  311. `,
  312. ({ get }) => {
  313. get('h1:nth-of-type(1) h2:nth-of-type(1)').should(haveText('foo: bob'))
  314. get('h1:nth-of-type(1) h2:nth-of-type(2)').should(haveText('foo: lob'))
  315. get('h1:nth-of-type(2) h2:nth-of-type(1)').should(haveText('baz: bab'))
  316. get('h1:nth-of-type(2) h2:nth-of-type(2)').should(haveText('baz: lab'))
  317. }
  318. )
  319. test('sibling x-for do not interact with each other',
  320. html`
  321. <div x-data="{ foos: [1], bars: [1, 2] }">
  322. <template x-for="foo in foos">
  323. <h1 x-text="foo"></h1>
  324. </template>
  325. <template x-for="bar in bars">
  326. <h2 x-text="bar"></h2>
  327. </template>
  328. <button @click="foos = [1, 2];bars = [1, 2, 3]">Change</button>
  329. </div>
  330. `,
  331. ({ get }) => {
  332. get('h1:nth-of-type(1)').should(haveText('1'))
  333. get('h2:nth-of-type(1)').should(haveText('1'))
  334. get('h2:nth-of-type(2)').should(haveText('2'))
  335. get('button').click()
  336. get('h1:nth-of-type(1)').should(haveText('1'))
  337. get('h1:nth-of-type(2)').should(haveText('2'))
  338. get('h2:nth-of-type(1)').should(haveText('1'))
  339. get('h2:nth-of-type(2)').should(haveText('2'))
  340. get('h2:nth-of-type(3)').should(haveText('3'))
  341. }
  342. )
  343. test('x-for over range using i in x syntax',
  344. html`
  345. <div x-data>
  346. <template x-for="i in 10">
  347. <span x-text="i"></span>
  348. </template>
  349. </div>
  350. `,
  351. ({ get }) => get('span').should(haveLength('10'))
  352. )
  353. test('x-for over range using i in property syntax',
  354. html`
  355. <div x-data="{ count: 10 }">
  356. <template x-for="i in count">
  357. <span x-text="i"></span>
  358. </template>
  359. </div>
  360. `,
  361. ({ get }) => get('span').should(haveLength('10'))
  362. )
  363. // @flaky
  364. test.retry(2)('x-for with an array of numbers',
  365. `
  366. <div x-data="{ items: [] }">
  367. <template x-for="i in items">
  368. <span x-text="i"></span>
  369. </template>
  370. <button @click="items.push(2)" id="first">click me</button>
  371. <button @click="items.push(3)" id="second">click me</button>
  372. </div>
  373. `,
  374. ({ get }) => {
  375. get('span').should(haveLength('0'))
  376. get('#first').click()
  377. get('span').should(haveLength('1'))
  378. get('#second').click()
  379. get('span').should(haveLength('2'))
  380. }
  381. )