1
0

bind.spec.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. import Alpine from 'alpinejs'
  2. import { wait } from '@testing-library/dom'
  3. global.MutationObserver = class {
  4. observe() {}
  5. }
  6. test('attribute bindings are set on initialize', async () => {
  7. document.body.innerHTML = `
  8. <div x-data="{ foo: 'bar' }">
  9. <span x-bind:foo="foo"></span>
  10. </div>
  11. `
  12. Alpine.start()
  13. expect(document.querySelector('span').getAttribute('foo')).toEqual('bar')
  14. })
  15. test('class attribute bindings are merged by string syntax', async () => {
  16. document.body.innerHTML = `
  17. <div x-data="{ isOn: false }">
  18. <span class="foo" x-bind:class="isOn ? 'bar': ''"></span>
  19. <button @click="isOn = ! isOn"></button>
  20. </div>
  21. `
  22. Alpine.start()
  23. expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
  24. expect(document.querySelector('span').classList.contains('bar')).toBeFalsy()
  25. document.querySelector('button').click()
  26. await wait(() => {
  27. expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
  28. expect(document.querySelector('span').classList.contains('bar')).toBeTruthy()
  29. })
  30. document.querySelector('button').click()
  31. await wait(() => {
  32. expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
  33. expect(document.querySelector('span').classList.contains('bar')).toBeFalsy()
  34. })
  35. })
  36. test('class attribute bindings are merged by array syntax', async () => {
  37. document.body.innerHTML = `
  38. <div x-data="{ isOn: false }">
  39. <span class="foo" x-bind:class="isOn ? ['bar', 'baz']: ['bar']"></span>
  40. <button @click="isOn = ! isOn"></button>
  41. </div>
  42. `
  43. Alpine.start()
  44. expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
  45. expect(document.querySelector('span').classList.contains('bar')).toBeTruthy()
  46. expect(document.querySelector('span').classList.contains('baz')).toBeFalsy()
  47. document.querySelector('button').click()
  48. await wait(() => {
  49. expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
  50. expect(document.querySelector('span').classList.contains('bar')).toBeTruthy()
  51. expect(document.querySelector('span').classList.contains('baz')).toBeTruthy()
  52. })
  53. document.querySelector('button').click()
  54. await wait(() => {
  55. expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
  56. expect(document.querySelector('span').classList.contains('bar')).toBeTruthy()
  57. expect(document.querySelector('span').classList.contains('baz')).toBeFalsy()
  58. })
  59. })
  60. test('class attribute bindings are removed by object syntax', async () => {
  61. document.body.innerHTML = `
  62. <div x-data="{ isOn: false }">
  63. <span class="foo" x-bind:class="{ 'foo': isOn }"></span>
  64. </div>
  65. `
  66. Alpine.start()
  67. expect(document.querySelector('span').classList.contains('foo')).toBeFalsy()
  68. })
  69. test('class attribute bindings are added by string syntax', async () => {
  70. document.body.innerHTML = `
  71. <div x-data="{ initialClass: 'foo' }">
  72. <span x-bind:class="initialClass"></span>
  73. </div>
  74. `
  75. Alpine.start()
  76. expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
  77. })
  78. test('class attribute bindings are added by object syntax', async () => {
  79. document.body.innerHTML = `
  80. <div x-data="{ isOn: true }">
  81. <span x-bind:class="{ 'foo': isOn }"></span>
  82. </div>
  83. `
  84. Alpine.start()
  85. expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
  86. })
  87. test('multiple classes are removed by object syntax', async () => {
  88. document.body.innerHTML = `
  89. <div x-data="{ isOn: false }">
  90. <span class="foo bar" x-bind:class="{ 'foo bar': isOn }"></span>
  91. </div>
  92. `
  93. Alpine.start()
  94. expect(document.querySelector('span').classList.contains('foo')).toBeFalsy()
  95. expect(document.querySelector('span').classList.contains('bar')).toBeFalsy()
  96. })
  97. test('multiple classes are added by object syntax', async () => {
  98. document.body.innerHTML = `
  99. <div x-data="{ isOn: true }">
  100. <span x-bind:class="{ 'foo bar': isOn }"></span>
  101. </div>
  102. `
  103. Alpine.start()
  104. expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
  105. expect(document.querySelector('span').classList.contains('bar')).toBeTruthy()
  106. })
  107. test('class attribute bindings are added by nested object syntax', async () => {
  108. document.body.innerHTML = `
  109. <div x-data="{ nested: { isOn: true } }">
  110. <span x-bind:class="{ 'foo': nested.isOn }"></span>
  111. </div>
  112. `
  113. Alpine.start()
  114. expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
  115. })
  116. test('class attribute bindings are added by array syntax', async () => {
  117. document.body.innerHTML = `
  118. <div x-data="{}">
  119. <span class="" x-bind:class="['foo']"></span>
  120. </div>
  121. `
  122. Alpine.start()
  123. expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
  124. })
  125. test('class attribute bindings are synced by string syntax', async () => {
  126. document.body.innerHTML = `
  127. <div x-data="{foo: 'bar baz'}">
  128. <span class="" x-bind:class="foo"></span>
  129. </div>
  130. `
  131. Alpine.start()
  132. expect(document.querySelector('span').classList.contains('bar')).toBeTruthy()
  133. expect(document.querySelector('span').classList.contains('baz')).toBeTruthy()
  134. })
  135. test('non-boolean attributes set to null/undefined/false are removed from the element', async () => {
  136. document.body.innerHTML = `
  137. <div x-data="{}">
  138. <a href="#hello" x-bind:href="null"></a>
  139. <a href="#hello" x-bind:href="false"></a>
  140. <a href="#hello" x-bind:href="undefined"></a>
  141. <!-- custom attribute see https://github.com/alpinejs/alpine/issues/280 -->
  142. <span visible="true" x-bind:visible="null"></span>
  143. <span visible="true" x-bind:visible="false"></span>
  144. <span visible="true" x-bind:visible="undefined"></span>
  145. </div>
  146. `
  147. Alpine.start()
  148. expect(document.querySelectorAll('a')[0].getAttribute('href')).toBeNull()
  149. expect(document.querySelectorAll('a')[1].getAttribute('href')).toBeNull()
  150. expect(document.querySelectorAll('a')[2].getAttribute('href')).toBeNull()
  151. expect(document.querySelectorAll('span')[0].getAttribute('visible')).toBeNull()
  152. expect(document.querySelectorAll('span')[1].getAttribute('visible')).toBeNull()
  153. expect(document.querySelectorAll('span')[2].getAttribute('visible')).toBeNull()
  154. })
  155. test('non-boolean empty string attributes are not removed', async () => {
  156. document.body.innerHTML = `
  157. <div x-data="{}">
  158. <a href="#hello" x-bind:href="''"></a>
  159. </div>
  160. `
  161. Alpine.start()
  162. expect(document.querySelectorAll('a')[0].getAttribute('href')).toEqual('')
  163. })
  164. test('truthy boolean attribute values are set to their attribute name', async () => {
  165. document.body.innerHTML = `
  166. <div x-data="{ isSet: true }">
  167. <input x-bind:disabled="isSet"></input>
  168. <input x-bind:checked="isSet"></input>
  169. <input x-bind:required="isSet"></input>
  170. <input x-bind:readonly="isSet"></input>
  171. <details x-bind:open="isSet"></details>
  172. <select x-bind:multiple="isSet"></select>
  173. <option x-bind:selected="isSet"></option>
  174. <textarea x-bind:autofocus="isSet"></textarea>
  175. <dl x-bind:itemscope="isSet"></dl>
  176. <form x-bind:novalidate="isSet"></form>
  177. <iframe
  178. x-bind:allowfullscreen="isSet"
  179. x-bind:allowpaymentrequest="isSet"
  180. ></iframe>
  181. <button x-bind:formnovalidate="isSet"></button>
  182. <audio
  183. x-bind:autoplay="isSet"
  184. x-bind:controls="isSet"
  185. x-bind:loop="isSet"
  186. x-bind:muted="isSet"
  187. ></audio>
  188. <video x-bind:playsinline="isSet"></video>
  189. <track x-bind:default="isSet" />
  190. <img x-bind:ismap="isSet" />
  191. <ol x-bind:reversed="isSet"></ol>
  192. <script
  193. x-bind:async="isSet"
  194. x-bind:defer="isSet"
  195. x-bind:nomodule="isSet"
  196. ></script>
  197. </div>
  198. `
  199. Alpine.start()
  200. expect(document.querySelectorAll('input')[0].disabled).toBeTruthy()
  201. expect(document.querySelectorAll('input')[1].checked).toBeTruthy()
  202. expect(document.querySelectorAll('input')[2].required).toBeTruthy()
  203. expect(document.querySelectorAll('input')[3].readOnly).toBeTruthy()
  204. expect(document.querySelectorAll('details')[0].open).toBeTruthy()
  205. expect(document.querySelectorAll('option')[0].selected).toBeTruthy()
  206. expect(document.querySelectorAll('select')[0].multiple).toBeTruthy()
  207. expect(document.querySelectorAll('textarea')[0].autofocus).toBeTruthy()
  208. expect(document.querySelectorAll('dl')[0].attributes.itemscope).toBeTruthy()
  209. expect(document.querySelectorAll('form')[0].attributes.novalidate).toBeTruthy()
  210. expect(document.querySelectorAll('iframe')[0].attributes.allowfullscreen).toBeTruthy()
  211. expect(document.querySelectorAll('iframe')[0].attributes.allowpaymentrequest).toBeTruthy()
  212. expect(document.querySelectorAll('button')[0].attributes.formnovalidate).toBeTruthy()
  213. expect(document.querySelectorAll('audio')[0].attributes.autoplay).toBeTruthy()
  214. expect(document.querySelectorAll('audio')[0].attributes.controls).toBeTruthy()
  215. expect(document.querySelectorAll('audio')[0].attributes.loop).toBeTruthy()
  216. expect(document.querySelectorAll('audio')[0].attributes.muted).toBeTruthy()
  217. expect(document.querySelectorAll('video')[0].attributes.playsinline).toBeTruthy()
  218. expect(document.querySelectorAll('track')[0].attributes.default).toBeTruthy()
  219. expect(document.querySelectorAll('img')[0].attributes.ismap).toBeTruthy()
  220. expect(document.querySelectorAll('ol')[0].attributes.reversed).toBeTruthy()
  221. expect(document.querySelectorAll('script')[0].attributes.async).toBeTruthy()
  222. expect(document.querySelectorAll('script')[0].attributes.defer).toBeTruthy()
  223. expect(document.querySelectorAll('script')[0].attributes.nomodule).toBeTruthy()
  224. })
  225. test('null, undefined, or false boolean attribute values are removed', async () => {
  226. document.body.innerHTML = `
  227. <div x-data="{ isSet: false }">
  228. <input x-bind:disabled="isSet"></input>
  229. <input x-bind:checked="isSet"></input>
  230. <input x-bind:required="isSet"></input>
  231. <input x-bind:readonly="isSet"></input>
  232. <input x-bind:hidden="isSet"></input>
  233. <details x-bind:open="isSet"></details>
  234. <select x-bind:multiple="isSet"></select>
  235. <option x-bind:selected="isSet"></option>
  236. <textarea x-bind:autofocus="isSet"></textarea>
  237. <dl x-bind:itemscope="isSet"></dl>
  238. <form x-bind:novalidate="isSet"></form>
  239. <iframe
  240. x-bind:allowfullscreen="isSet"
  241. x-bind:allowpaymentrequest="isSet"
  242. ></iframe>
  243. <button x-bind:formnovalidate="isSet"></button>
  244. <audio
  245. x-bind:autoplay="isSet"
  246. x-bind:controls="isSet"
  247. x-bind:loop="isSet"
  248. x-bind:muted="isSet"
  249. ></audio>
  250. <video x-bind:playsinline="isSet"></video>
  251. <track x-bind:default="isSet" />
  252. <img x-bind:ismap="isSet" />
  253. <ol x-bind:reversed="isSet"></ol>
  254. <script
  255. x-bind:async="isSet"
  256. x-bind:defer="isSet"
  257. x-bind:nomodule="isSet"
  258. ></script>
  259. </div>
  260. `
  261. Alpine.start()
  262. expect(document.querySelectorAll('input')[0].getAttribute('disabled')).toBeNull()
  263. expect(document.querySelectorAll('input')[1].getAttribute('checked')).toBeNull()
  264. expect(document.querySelectorAll('input')[2].getAttribute('required')).toBeNull()
  265. expect(document.querySelectorAll('input')[3].getAttribute('readOnly')).toBeNull()
  266. expect(document.querySelectorAll('input')[4].getAttribute('hidden')).toBeNull()
  267. expect(document.querySelectorAll('details')[0].getAttribute('open')).toBeNull()
  268. expect(document.querySelectorAll('option')[0].getAttribute('selected')).toBeNull()
  269. expect(document.querySelectorAll('select')[0].getAttribute('multiple')).toBeNull()
  270. expect(document.querySelectorAll('textarea')[0].getAttribute('autofocus')).toBeNull()
  271. expect(document.querySelectorAll('dl')[0].getAttribute('itemscope')).toBeNull()
  272. expect(document.querySelectorAll('form')[0].getAttribute('novalidate')).toBeNull()
  273. expect(document.querySelectorAll('iframe')[0].getAttribute('allowfullscreen')).toBeNull()
  274. expect(document.querySelectorAll('iframe')[0].getAttribute('allowpaymentrequest')).toBeNull()
  275. expect(document.querySelectorAll('button')[0].getAttribute('formnovalidate')).toBeNull()
  276. expect(document.querySelectorAll('audio')[0].getAttribute('autoplay')).toBeNull()
  277. expect(document.querySelectorAll('audio')[0].getAttribute('controls')).toBeNull()
  278. expect(document.querySelectorAll('audio')[0].getAttribute('loop')).toBeNull()
  279. expect(document.querySelectorAll('audio')[0].getAttribute('muted')).toBeNull()
  280. expect(document.querySelectorAll('video')[0].getAttribute('playsinline')).toBeNull()
  281. expect(document.querySelectorAll('track')[0].getAttribute('default')).toBeNull()
  282. expect(document.querySelectorAll('img')[0].getAttribute('ismap')).toBeNull()
  283. expect(document.querySelectorAll('ol')[0].getAttribute('reversed')).toBeNull()
  284. expect(document.querySelectorAll('script')[0].getAttribute('async')).toBeNull()
  285. expect(document.querySelectorAll('script')[0].getAttribute('defer')).toBeNull()
  286. expect(document.querySelectorAll('script')[0].getAttribute('nomodule')).toBeNull()
  287. })
  288. test('boolean empty string attributes are not removed', async () => {
  289. document.body.innerHTML = `
  290. <div x-data="{}">
  291. <input x-bind:disabled="''">
  292. </div>
  293. `
  294. Alpine.start()
  295. expect(document.querySelectorAll('input')[0].disabled).toEqual(true)
  296. })
  297. test('binding supports short syntax', async () => {
  298. document.body.innerHTML = `
  299. <div x-data="{ foo: 'bar' }">
  300. <span :class="foo"></span>
  301. </div>
  302. `
  303. Alpine.start()
  304. expect(document.querySelector('span').classList.contains('bar')).toBeTruthy()
  305. })
  306. test('checkbox is unchecked by default', async () => {
  307. document.body.innerHTML = `
  308. <div x-data="{foo: {bar: 'baz'}}">
  309. <input type="checkbox" x-bind:value="''"></input>
  310. <input type="checkbox" x-bind:value="'test'"></input>
  311. <input type="checkbox" x-bind:value="foo.bar"></input>
  312. <input type="checkbox" x-bind:value="0"></input>
  313. <input type="checkbox" x-bind:value="10"></input>
  314. </div>
  315. `
  316. Alpine.start()
  317. expect(document.querySelectorAll('input')[0].checked).toBeFalsy()
  318. expect(document.querySelectorAll('input')[1].checked).toBeFalsy()
  319. expect(document.querySelectorAll('input')[2].checked).toBeFalsy()
  320. expect(document.querySelectorAll('input')[3].checked).toBeFalsy()
  321. expect(document.querySelectorAll('input')[4].checked).toBeFalsy()
  322. })
  323. test('radio is unchecked by default', async () => {
  324. document.body.innerHTML = `
  325. <div x-data="{foo: {bar: 'baz'}}">
  326. <input type="radio" x-bind:value="''"></input>
  327. <input type="radio" x-bind:value="'test'"></input>
  328. <input type="radio" x-bind:value="foo.bar"></input>
  329. <input type="radio" x-bind:value="0"></input>
  330. <input type="radio" x-bind:value="10"></input>
  331. </div>
  332. `
  333. Alpine.start()
  334. expect(document.querySelectorAll('input')[0].checked).toBeFalsy()
  335. expect(document.querySelectorAll('input')[1].checked).toBeFalsy()
  336. expect(document.querySelectorAll('input')[2].checked).toBeFalsy()
  337. expect(document.querySelectorAll('input')[3].checked).toBeFalsy()
  338. expect(document.querySelectorAll('input')[4].checked).toBeFalsy()
  339. })
  340. test('checkbox values are set correctly', async () => {
  341. document.body.innerHTML = `
  342. <div x-data="{ stringValue: 'foo', trueValue: true, falseValue: false }">
  343. <input type="checkbox" name="stringCheckbox" :value="stringValue" />
  344. <input type="checkbox" name="trueCheckbox" :value="trueValue" />
  345. <input type="checkbox" name="falseCheckbox" :value="falseValue" />
  346. </div>
  347. `
  348. Alpine.start()
  349. expect(document.querySelector('input[name="trueCheckbox"]').value).toEqual('on')
  350. expect(document.querySelector('input[name="falseCheckbox"]').value).toEqual('on')
  351. expect(document.querySelector('input[name="stringCheckbox"]').value).toEqual('foo')
  352. });
  353. test('radio values are set correctly', async () => {
  354. document.body.innerHTML = `
  355. <div x-data="{lists: [{id: 1}, {id: 8}], selectedListID: '8'}">
  356. <template x-for="list in lists" :key="list.id">
  357. <input x-model="selectedListID" type="radio" :value="list.id.toString()" :id="'list-' + list.id">
  358. </template>
  359. <input type="radio" id="list-test" value="test" x-model="selectedListID">
  360. </div>
  361. `
  362. Alpine.start()
  363. expect(document.querySelector('#list-1').value).toEqual('1')
  364. expect(document.querySelector('#list-1').checked).toBeFalsy()
  365. expect(document.querySelector('#list-8').value).toEqual('8')
  366. expect(document.querySelector('#list-8').checked).toBeTruthy()
  367. expect(document.querySelector('#list-test').value).toEqual('test')
  368. expect(document.querySelector('#list-test').checked).toBeFalsy()
  369. });
  370. test('classes are removed before being added', async () => {
  371. document.body.innerHTML = `
  372. <div x-data="{ isOpen: true }">
  373. <span :class="{ 'text-red block': isOpen, 'text-red hidden': !isOpen }">
  374. Span
  375. </span>
  376. <button @click="isOpen = !isOpen"></button>
  377. </div>
  378. `
  379. Alpine.start()
  380. expect(document.querySelector('span').classList.contains('block')).toBeTruthy()
  381. expect(document.querySelector('span').classList.contains('text-red')).toBeTruthy()
  382. document.querySelector('button').click()
  383. await wait(() => {
  384. expect(document.querySelector('span').classList.contains('block')).toBeFalsy()
  385. expect(document.querySelector('span').classList.contains('hidden')).toBeTruthy()
  386. expect(document.querySelector('span').classList.contains('text-red')).toBeTruthy()
  387. })
  388. });
  389. test('extra whitespace in class binding object syntax is ignored', async () => {
  390. document.body.innerHTML = `
  391. <div x-data>
  392. <span x-bind:class="{ ' foo bar ': true }"></span>
  393. </div>
  394. `
  395. Alpine.start()
  396. expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
  397. expect(document.querySelector('span').classList.contains('bar')).toBeTruthy()
  398. })
  399. test('extra whitespace in class binding string syntax is ignored', async () => {
  400. document.body.innerHTML = `
  401. <div x-data>
  402. <span x-bind:class="' foo bar '"></span>
  403. </div>
  404. `
  405. Alpine.start()
  406. expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
  407. expect(document.querySelector('span').classList.contains('bar')).toBeTruthy()
  408. })
  409. test('undefined class binding resolves to empty string', async () => {
  410. jest.spyOn(window, 'setTimeout').mockImplementation((callback,time) => {
  411. callback()
  412. });
  413. document.body.innerHTML = `
  414. <div x-data="{ errorClass: (hasError) => { if (hasError) { return 'red' } } }">
  415. <span id="error" x-bind:class="errorClass(true)">should be red</span>
  416. <span id="empty" x-bind:class="errorClass(false)">should be empty</span>
  417. </div>
  418. `
  419. await expect(Alpine.start()).resolves.toBeUndefined()
  420. expect(document.querySelector('#error').classList.value).toEqual('red')
  421. expect(document.querySelector('#empty').classList.value).toEqual('')
  422. })
  423. test('.camel modifier correctly sets name of attribute', async () => {
  424. document.body.innerHTML = `
  425. <div x-data>
  426. <svg x-bind:view-box.camel="'0 0 42 42'"></svg>
  427. </div>
  428. `
  429. Alpine.start()
  430. expect(document.querySelector('svg').getAttribute('viewBox')).toEqual('0 0 42 42')
  431. })
  432. test('attribute binding names can contain numbers', async () => {
  433. document.body.innerHTML = `
  434. <svg x-data>
  435. <line x1="1" y1="2" :x2="3" x-bind:y2="4" />
  436. </svg>
  437. `;
  438. Alpine.start();
  439. expect(document.querySelector('line').getAttribute('x2')).toEqual('3');
  440. expect(document.querySelector('line').getAttribute('y2')).toEqual('4');
  441. })
  442. test('non-string and non-boolean attributes are cast to string when bound to checkbox', () => {
  443. document.body.innerHTML = `
  444. <div x-data="{ number: 100, zero: 0, bool: true, nullProp: null }">
  445. <input type="checkbox" id="number" :value="number">
  446. <input type="checkbox" id="zero" :value="zero">
  447. <input type="checkbox" id="boolean" :value="bool">
  448. <input type="checkbox" id="null" :value="nullProp">
  449. </div>
  450. `
  451. Alpine.start()
  452. expect(document.querySelector('#number').value).toEqual('100')
  453. expect(document.querySelector('#zero').value).toEqual('0')
  454. expect(document.querySelector('#boolean').value).toEqual('on')
  455. expect(document.querySelector('#null').value).toEqual('on')
  456. })