x-bind.spec.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. import { beHidden, beVisible, haveText, beChecked, haveAttribute, haveClasses, haveProperty, haveValue, notBeChecked, notHaveAttribute, notHaveClasses, test, html } from '../../utils';
  2. test('sets attribute bindings on initialize',
  3. html`
  4. <div x-data="{ foo: 'bar' }">
  5. <span x-ref="me" x-bind:foo="foo">[Subject]</span>
  6. </div>
  7. `,
  8. ({ get }) => get('span').should(haveAttribute('foo', 'bar'))
  9. )
  10. test('sets undefined nested keys to empty string',
  11. html`
  12. <div x-data="{ nested: {} }">
  13. <span x-bind:foo="nested.field">
  14. </div>
  15. `,
  16. ({ get }) => get('span').should(haveAttribute('foo', ''))
  17. )
  18. test('style attribute bindings are added by string syntax',
  19. html`
  20. <div x-data="{ initialClass: 'foo' }">
  21. <span x-bind:class="initialClass"></span>
  22. </div>
  23. `,
  24. ({ get }) => get('span').should(haveClasses(['foo']))
  25. )
  26. test('aria-pressed/checked/expanded/selected attribute boolean values are cast to a true/false string',
  27. html`
  28. <div x-data="{ open: true }">
  29. <span x-bind:aria-pressed="open"></span>
  30. <span x-bind:aria-checked="open"></span>
  31. <span x-bind:aria-expanded="open"></span>
  32. <span x-bind:aria-selected="open"></span>
  33. <span x-bind:aria-pressed="false"></span>
  34. <span x-bind:aria-checked="false"></span>
  35. <span x-bind:aria-expanded="false"></span>
  36. <span x-bind:aria-selected="false"></span>
  37. </div>
  38. `,
  39. ({ get }) => {
  40. get('span:nth-of-type(1)').should(haveAttribute('aria-pressed', 'true'))
  41. get('span:nth-of-type(2)').should(haveAttribute('aria-checked', 'true'))
  42. get('span:nth-of-type(3)').should(haveAttribute('aria-expanded', 'true'))
  43. get('span:nth-of-type(4)').should(haveAttribute('aria-selected', 'true'))
  44. get('span:nth-of-type(5)').should(haveAttribute('aria-pressed', 'false'))
  45. get('span:nth-of-type(6)').should(haveAttribute('aria-checked', 'false'))
  46. get('span:nth-of-type(7)').should(haveAttribute('aria-expanded', 'false'))
  47. get('span:nth-of-type(8)').should(haveAttribute('aria-selected', 'false'))
  48. }
  49. )
  50. test('non-boolean attributes set to null/undefined/false are removed from the element',
  51. html`
  52. <div x-data="{}">
  53. <a href="#hello" x-bind:href="null">null</a>
  54. <a href="#hello" x-bind:href="false">false</a>
  55. <a href="#hello" x-bind:href="undefined">undefined</a>
  56. <!-- custom attribute see https://github.com/alpinejs/alpine/issues/280 -->
  57. <span visible="true" x-bind:visible="null">null</span>
  58. <span visible="true" x-bind:visible="false">false</span>
  59. <span visible="true" x-bind:visible="undefined">undefined</span>
  60. <span hidden="true" x-bind:hidden="null">null</span>
  61. <span hidden="true" x-bind:hidden="false">false</span>
  62. <span hidden="true" x-bind:hidden="undefined">undefined</span>
  63. </div>
  64. `,
  65. ({ get }) => {
  66. get('a:nth-of-type(1)').should(notHaveAttribute('href'))
  67. get('a:nth-of-type(2)').should(notHaveAttribute('href'))
  68. get('a:nth-of-type(3)').should(notHaveAttribute('href'))
  69. get('span:nth-of-type(1)').should(notHaveAttribute('visible'))
  70. get('span:nth-of-type(2)').should(notHaveAttribute('visible'))
  71. get('span:nth-of-type(3)').should(notHaveAttribute('visible'))
  72. get('span:nth-of-type(4)').should(notHaveAttribute('hidden'))
  73. get('span:nth-of-type(5)').should(notHaveAttribute('hidden'))
  74. get('span:nth-of-type(6)').should(notHaveAttribute('hidden'))
  75. }
  76. )
  77. test('non-boolean empty string attributes are not removed',
  78. html`
  79. <div x-data>
  80. <a href="#hello" x-bind:href="''"></a>
  81. </div>
  82. `,
  83. ({ get }) => get('a').should(haveAttribute('href', ''))
  84. )
  85. test('boolean attribute values are set to their attribute name if true and removed if false',
  86. html`
  87. <div x-data="{ isSet: true }">
  88. <span @click="isSet = false" id="setToFalse">Set To False</span>
  89. <input x-bind:disabled="isSet"></input>
  90. <input x-bind:checked="isSet"></input>
  91. <input x-bind:required="isSet"></input>
  92. <input x-bind:readonly="isSet"></input>
  93. <details x-bind:open="isSet"></details>
  94. <select x-bind:multiple="isSet"></select>
  95. <option x-bind:selected="isSet"></option>
  96. <textarea x-bind:autofocus="isSet"></textarea>
  97. <dl x-bind:itemscope="isSet"></dl>
  98. <form x-bind:novalidate="isSet"></form>
  99. <iframe
  100. x-bind:allowfullscreen="isSet"
  101. x-bind:allowpaymentrequest="isSet"
  102. ></iframe>
  103. <button x-bind:formnovalidate="isSet"></button>
  104. <audio
  105. x-bind:autoplay="isSet"
  106. x-bind:controls="isSet"
  107. x-bind:loop="isSet"
  108. x-bind:muted="isSet"
  109. ></audio>
  110. <video x-bind:playsinline="isSet"></video>
  111. <track x-bind:default="isSet" />
  112. <img x-bind:ismap="isSet" />
  113. <ol x-bind:reversed="isSet"></ol>
  114. </div>
  115. `,
  116. ({ get }) => {
  117. get('input:nth-of-type(1)').should(haveAttribute('disabled', 'disabled'))
  118. get('input:nth-of-type(2)').should(haveAttribute('checked', 'checked'))
  119. get('input:nth-of-type(3)').should(haveAttribute('required', 'required'))
  120. get('input:nth-of-type(4)').should(haveAttribute('readonly', 'readonly'))
  121. get('details').should(haveAttribute('open', 'open'))
  122. get('select').should(haveAttribute('multiple', 'multiple'))
  123. get('option').should(haveAttribute('selected', 'selected'))
  124. get('textarea').should(haveAttribute('autofocus', 'autofocus'))
  125. get('dl').should(haveAttribute('itemscope', 'itemscope'))
  126. get('form').should(haveAttribute('novalidate', 'novalidate'))
  127. get('iframe').should(haveAttribute('allowfullscreen', 'allowfullscreen'))
  128. get('iframe').should(haveAttribute('allowpaymentrequest', 'allowpaymentrequest'))
  129. get('button').should(haveAttribute('formnovalidate', 'formnovalidate'))
  130. get('audio').should(haveAttribute('autoplay', 'autoplay'))
  131. get('audio').should(haveAttribute('controls', 'controls'))
  132. get('audio').should(haveAttribute('loop', 'loop'))
  133. get('audio').should(haveAttribute('muted', 'muted'))
  134. get('video').should(haveAttribute('playsinline', 'playsinline'))
  135. get('track').should(haveAttribute('default', 'default'))
  136. get('img').should(haveAttribute('ismap', 'ismap'))
  137. get('ol').should(haveAttribute('reversed', 'reversed'))
  138. get('#setToFalse').click()
  139. get('input:nth-of-type(1)').should(notHaveAttribute('disabled'))
  140. get('input:nth-of-type(2)').should(notHaveAttribute('checked'))
  141. get('input:nth-of-type(3)').should(notHaveAttribute('required'))
  142. get('input:nth-of-type(4)').should(notHaveAttribute('readonly'))
  143. get('details').should(notHaveAttribute('open'))
  144. get('select').should(notHaveAttribute('multiple'))
  145. get('option').should(notHaveAttribute('selected'))
  146. get('textarea').should(notHaveAttribute('autofocus'))
  147. get('dl').should(notHaveAttribute('itemscope'))
  148. get('form').should(notHaveAttribute('novalidate'))
  149. get('iframe').should(notHaveAttribute('allowfullscreen'))
  150. get('iframe').should(notHaveAttribute('allowpaymentrequest'))
  151. get('button').should(notHaveAttribute('formnovalidate'))
  152. get('audio').should(notHaveAttribute('autoplay'))
  153. get('audio').should(notHaveAttribute('controls'))
  154. get('audio').should(notHaveAttribute('loop'))
  155. get('audio').should(notHaveAttribute('muted'))
  156. get('video').should(notHaveAttribute('playsinline'))
  157. get('track').should(notHaveAttribute('default'))
  158. get('img').should(notHaveAttribute('ismap'))
  159. get('ol').should(notHaveAttribute('reversed'))
  160. get('script').should(notHaveAttribute('async'))
  161. get('script').should(notHaveAttribute('defer'))
  162. get('script').should(notHaveAttribute('nomodule'))
  163. }
  164. )
  165. test('boolean empty string attributes are not removed',
  166. html`
  167. <div x-data="{}">
  168. <input x-bind:disabled="''">
  169. </div>
  170. `,
  171. ({ get }) => get('input').should(haveAttribute('disabled', 'disabled'))
  172. )
  173. test('binding supports short syntax',
  174. html`
  175. <div x-data="{ foo: 'bar' }">
  176. <span :class="foo"></span>
  177. </div>
  178. `,
  179. ({ get }) => get('span').should(haveClasses(['bar']))
  180. )
  181. test('checkbox is unchecked by default',
  182. html`
  183. <div x-data="{foo: {bar: 'baz'}}">
  184. <input type="checkbox" x-bind:value="''"></input>
  185. <input type="checkbox" x-bind:value="'test'"></input>
  186. <input type="checkbox" x-bind:value="foo.bar"></input>
  187. <input type="checkbox" x-bind:value="0"></input>
  188. <input type="checkbox" x-bind:value="10"></input>
  189. </div>
  190. `,
  191. ({ get }) => {
  192. get('input:nth-of-type(1)').should(notBeChecked())
  193. get('input:nth-of-type(2)').should(notBeChecked())
  194. get('input:nth-of-type(3)').should(notBeChecked())
  195. get('input:nth-of-type(4)').should(notBeChecked())
  196. get('input:nth-of-type(5)').should(notBeChecked())
  197. }
  198. )
  199. test('radio is unchecked by default',
  200. html`
  201. <div x-data="{foo: {bar: 'baz'}}">
  202. <input type="radio" x-bind:value="''"></input>
  203. <input type="radio" x-bind:value="'test'"></input>
  204. <input type="radio" x-bind:value="foo.bar"></input>
  205. <input type="radio" x-bind:value="0"></input>
  206. <input type="radio" x-bind:value="10"></input>
  207. </div>
  208. `,
  209. ({ get }) => {
  210. get('input:nth-of-type(1)').should(notBeChecked())
  211. get('input:nth-of-type(2)').should(notBeChecked())
  212. get('input:nth-of-type(3)').should(notBeChecked())
  213. get('input:nth-of-type(4)').should(notBeChecked())
  214. get('input:nth-of-type(5)').should(notBeChecked())
  215. }
  216. )
  217. test('checkbox values are set correctly',
  218. html`
  219. <div x-data="{ stringValue: 'foo', trueValue: true, falseValue: false }">
  220. <input type="checkbox" name="stringCheckbox" :value="stringValue" />
  221. <input type="checkbox" name="trueCheckbox" :value="trueValue" />
  222. <input type="checkbox" name="falseCheckbox" :value="falseValue" />
  223. </div>
  224. `,
  225. ({ get }) => {
  226. get('input:nth-of-type(1)').should(haveValue('foo'))
  227. get('input:nth-of-type(2)').should(haveValue('on'))
  228. get('input:nth-of-type(3)').should(haveValue('on'))
  229. }
  230. )
  231. test('radio values are set correctly',
  232. html`
  233. <div x-data="{lists: [{id: 1}, {id: 8}], selectedListID: '8'}">
  234. <template x-for="list in lists" :key="list.id">
  235. <input x-model="selectedListID" type="radio" :value="list.id.toString()" :id="'list-' + list.id">
  236. </template>
  237. <input type="radio" id="list-test" value="test" x-model="selectedListID">
  238. </div>
  239. `,
  240. ({ get }) => {
  241. get('#list-1').should(haveValue('1'))
  242. get('#list-1').should(notBeChecked())
  243. get('#list-8').should(haveValue('8'))
  244. get('#list-8').should(beChecked())
  245. get('#list-test').should(haveValue('test'))
  246. get('#list-test').should(notBeChecked())
  247. }
  248. )
  249. test('.camel modifier correctly sets name of attribute',
  250. html`
  251. <div x-data>
  252. <svg x-bind:view-box.camel="'0 0 42 42'"></svg>
  253. </div>
  254. `,
  255. ({ get }) => get('svg').should(haveAttribute('viewBox', '0 0 42 42'))
  256. )
  257. test('attribute binding names can contain numbers',
  258. html`
  259. <svg x-data>
  260. <line x1="1" y1="2" :x2="3" x-bind:y2="4" />
  261. </svg>
  262. `,
  263. ({ get }) => {
  264. get('line').should(haveAttribute('x2', '3'))
  265. get('line').should(haveAttribute('y2', '4'))
  266. }
  267. )
  268. test('non-string and non-boolean attributes are cast to string when bound to checkbox',
  269. html`
  270. <div x-data="{ number: 100, zero: 0, bool: true, nullProp: null }">
  271. <input type="checkbox" id="number" :value="number">
  272. <input type="checkbox" id="zero" :value="zero">
  273. <input type="checkbox" id="boolean" :value="bool">
  274. <input type="checkbox" id="null" :value="nullProp">
  275. </div>
  276. `,
  277. ({ get }) => {
  278. get('input:nth-of-type(1)').should(haveValue('100'))
  279. get('input:nth-of-type(2)').should(haveValue('0'))
  280. get('input:nth-of-type(3)').should(haveValue('on'))
  281. get('input:nth-of-type(4)').should(haveValue('on'))
  282. }
  283. )
  284. test('can bind an object of directives',
  285. html`
  286. <script>
  287. window.modal = function () {
  288. return {
  289. foo: 'bar',
  290. trigger: {
  291. ['x-on:click']() { this.foo = 'baz' },
  292. },
  293. dialogue: {
  294. ['x-text']() { return this.foo },
  295. },
  296. }
  297. }
  298. </script>
  299. <div x-data="window.modal()">
  300. <button x-bind="trigger">Toggle</button>
  301. <span x-bind="dialogue">Modal Body</span>
  302. </div>
  303. `,
  304. ({ get }) => {
  305. get('span').should(haveText('bar'))
  306. get('button').click()
  307. get('span').should(haveText('baz'))
  308. }
  309. )
  310. test('x-bind object syntax supports normal HTML attributes',
  311. html`
  312. <span x-data x-bind="{ foo: 'bar' }"></span>
  313. `,
  314. ({ get }) => {
  315. get('span').should(haveAttribute('foo', 'bar'))
  316. }
  317. )
  318. test('x-bind object syntax supports normal HTML attributes mixed in with dynamic ones',
  319. html`
  320. <span x-data x-bind="{ 'x-bind:bob'() { return 'lob'; }, foo: 'bar', 'x-bind:bab'() { return 'lab' } }"></span>
  321. `,
  322. ({ get }) => {
  323. get('span').should(haveAttribute('foo', 'bar'))
  324. get('span').should(haveAttribute('bob', 'lob'))
  325. get('span').should(haveAttribute('bab', 'lab'))
  326. }
  327. )
  328. test('x-bind object syntax supports x-for',
  329. html`
  330. <script>
  331. window.todos = () => { return {
  332. todos: ['foo', 'bar'],
  333. outputForExpression: {
  334. ['x-for']: 'todo in todos',
  335. }
  336. }}
  337. </script>
  338. <div x-data="window.todos()">
  339. <ul>
  340. <template x-bind="outputForExpression">
  341. <li x-text="todo"></li>
  342. </template>
  343. </ul>
  344. </div>
  345. `,
  346. ({ get }) => {
  347. get('li:nth-of-type(1)').should(haveText('foo'))
  348. get('li:nth-of-type(2)').should(haveText('bar'))
  349. }
  350. )
  351. test('x-bind object syntax syntax supports x-transition',
  352. html`
  353. <style>
  354. .transition { transition-property: background-color, border-color, color, fill, stroke; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
  355. .duration-100 { transition-duration: 100ms; }
  356. </style>
  357. <script>
  358. window.transitions = () => { return {
  359. show: true,
  360. outputClickExpression: {
  361. ['@click']() { this.show = false },
  362. ['x-text']() { return 'Click Me' },
  363. },
  364. outputTransitionExpression: {
  365. ['x-show']() { return this.show },
  366. ['x-transition:enter']: 'transition duration-100',
  367. ['x-transition:leave']: 'transition duration-100',
  368. },
  369. }}
  370. </script>
  371. <div x-data="transitions()">
  372. <button x-bind="outputClickExpression"></button>
  373. <span x-bind="outputTransitionExpression">thing</span>
  374. </div>
  375. `,
  376. ({ get }) => {
  377. get('span').should(beVisible())
  378. get('button').click()
  379. get('span').should(beVisible())
  380. get('span').should(beHidden())
  381. }
  382. )
  383. test('x-bind object syntax event handlers defined as functions receive the event object as their first argument',
  384. html`
  385. <script>
  386. window.data = () => { return {
  387. button: {
  388. ['@click'](event) {
  389. this.$refs.span.innerText = event.currentTarget.id
  390. }
  391. }
  392. }}
  393. </script>
  394. <div x-data="window.data()">
  395. <button x-bind="button" id="bar">click me</button>
  396. <span x-ref="span">foo</span>
  397. </div>
  398. `,
  399. ({ get }) => {
  400. get('span').should(haveText('foo'))
  401. get('button').click()
  402. get('span').should(haveText('bar'))
  403. }
  404. )
  405. test('x-bind object syntax event handlers defined as functions receive element bound magics',
  406. html`
  407. <script>
  408. window.data = () => { return {
  409. button: {
  410. ['@click']() {
  411. this.$refs.span.innerText = this.$el.id
  412. }
  413. }
  414. }}
  415. </script>
  416. <div x-data="window.data()">
  417. <button x-bind="button" id="bar">click me</button>
  418. <span x-ref="span">foo</span>
  419. </div>
  420. `,
  421. ({ get }) => {
  422. get('span').should(haveText('foo'))
  423. get('button').click()
  424. get('span').should(haveText('bar'))
  425. }
  426. )
  427. test('Can retrieve Alpine bound data with global bound method',
  428. html`
  429. <div id="1" x-data foo="bar" x-text="Alpine.bound($el, 'foo')"></div>
  430. <div id="2" x-data :foo="'bar'" x-text="Alpine.bound($el, 'foo')"></div>
  431. <div id="3" x-data foo x-text="Alpine.bound($el, 'foo')"></div>
  432. <div id="4" x-data disabled x-text="Alpine.bound($el, 'disabled')"></div>
  433. <div id="5" x-data x-text="Alpine.bound($el, 'foo')"></div>
  434. <div id="6" x-data x-text="Alpine.bound($el, 'foo', 'bar')"></div>
  435. `,
  436. ({ get }) => {
  437. get('#1').should(haveText('bar'))
  438. get('#2').should(haveText('bar'))
  439. get('#3').should(haveText('true'))
  440. get('#4').should(haveText('true'))
  441. get('#5').should(haveText(''))
  442. get('#6').should(haveText('bar'))
  443. }
  444. )
  445. test('Can extract Alpine bound data as a data prop',
  446. html`
  447. <div x-data="{ foo: 'bar' }">
  448. <div id="1" x-data="{ init() { this.$el.textContent = Alpine.extractProp(this.$el, 'foo') }}" :foo="foo"></div>
  449. <div id="2" x-data="{ init() { this.$el.textContent = Alpine.extractProp(this.$el, 'foo', null, false) }}" :foo="foo"></div>
  450. </div>
  451. `,
  452. ({ get }) => {
  453. get('#1').should(haveText('bar'))
  454. get('#1').should(notHaveAttribute('foo'))
  455. get('#2').should(haveText('bar'))
  456. get('#2').should(haveAttribute('foo', 'bar'))
  457. }
  458. )
  459. test('x-bind updates checked attribute and property after user interaction',
  460. html`
  461. <div x-data="{ checked: true }">
  462. <button @click="checked = !checked">toggle</button>
  463. <input type="checkbox" x-bind:checked="checked" @change="checked = $event.target.checked" />
  464. </div>
  465. `,
  466. ({ get }) => {
  467. get('input').should(haveAttribute('checked', 'checked'))
  468. get('input').should(haveProperty('checked', true))
  469. get('input').click()
  470. get('input').should(notHaveAttribute('checked'))
  471. get('input').should(haveProperty('checked', false))
  472. get('button').click()
  473. get('input').should(haveAttribute('checked', 'checked'))
  474. get('input').should(haveProperty('checked', true))
  475. }
  476. )