1
0

x-bind.spec.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. import { beHidden, beVisible, haveText, beChecked, haveAttribute, haveClasses, 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 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. </div>
  31. `,
  32. ({ get }) => get('span').should(haveAttribute('aria-pressed', 'true'))
  33. )
  34. test('non-boolean attributes set to null/undefined/false are removed from the element',
  35. html`
  36. <div x-data="{}">
  37. <a href="#hello" x-bind:href="null">null</a>
  38. <a href="#hello" x-bind:href="false">false</a>
  39. <a href="#hello" x-bind:href="undefined">undefined</a>
  40. <!-- custom attribute see https://github.com/alpinejs/alpine/issues/280 -->
  41. <span visible="true" x-bind:visible="null">null</span>
  42. <span visible="true" x-bind:visible="false">false</span>
  43. <span visible="true" x-bind:visible="undefined">undefined</span>
  44. </div>
  45. `,
  46. ({ get }) => {
  47. get('a:nth-child(1)').should(notHaveAttribute('href'))
  48. get('a:nth-child(2)').should(notHaveAttribute('href'))
  49. get('a:nth-child(3)').should(notHaveAttribute('href'))
  50. get('span:nth-child(1)').should(notHaveAttribute('visible'))
  51. get('span:nth-child(2)').should(notHaveAttribute('visible'))
  52. get('span:nth-child(3)').should(notHaveAttribute('visible'))
  53. }
  54. )
  55. test('non-boolean empty string attributes are not removed',
  56. html`
  57. <div x-data>
  58. <a href="#hello" x-bind:href="''"></a>
  59. </div>
  60. `,
  61. ({ get }) => get('a').should(haveAttribute('href', ''))
  62. )
  63. test('boolean attribute values are set to their attribute name if true and removed if false',
  64. html`
  65. <div x-data="{ isSet: true }">
  66. <span @click="isSet = false" id="setToFalse">Set To False</span>
  67. <input x-bind:disabled="isSet"></input>
  68. <input x-bind:checked="isSet"></input>
  69. <input x-bind:required="isSet"></input>
  70. <input x-bind:readonly="isSet"></input>
  71. <details x-bind:open="isSet"></details>
  72. <select x-bind:multiple="isSet"></select>
  73. <option x-bind:selected="isSet"></option>
  74. <textarea x-bind:autofocus="isSet"></textarea>
  75. <dl x-bind:itemscope="isSet"></dl>
  76. <form x-bind:novalidate="isSet"></form>
  77. <iframe
  78. x-bind:allowfullscreen="isSet"
  79. x-bind:allowpaymentrequest="isSet"
  80. ></iframe>
  81. <button x-bind:formnovalidate="isSet"></button>
  82. <audio
  83. x-bind:autoplay="isSet"
  84. x-bind:controls="isSet"
  85. x-bind:loop="isSet"
  86. x-bind:muted="isSet"
  87. ></audio>
  88. <video x-bind:playsinline="isSet"></video>
  89. <track x-bind:default="isSet" />
  90. <img x-bind:ismap="isSet" />
  91. <ol x-bind:reversed="isSet"></ol>
  92. </div>
  93. `,
  94. ({ get }) => {
  95. get('input:nth-of-type(1)').should(haveAttribute('disabled', 'disabled'))
  96. get('input:nth-of-type(2)').should(haveAttribute('checked', 'checked'))
  97. get('input:nth-of-type(3)').should(haveAttribute('required', 'required'))
  98. get('input:nth-of-type(4)').should(haveAttribute('readonly', 'readonly'))
  99. get('details').should(haveAttribute('open', 'open'))
  100. get('select').should(haveAttribute('multiple', 'multiple'))
  101. get('option').should(haveAttribute('selected', 'selected'))
  102. get('textarea').should(haveAttribute('autofocus', 'autofocus'))
  103. get('dl').should(haveAttribute('itemscope', 'itemscope'))
  104. get('form').should(haveAttribute('novalidate', 'novalidate'))
  105. get('iframe').should(haveAttribute('allowfullscreen', 'allowfullscreen'))
  106. get('iframe').should(haveAttribute('allowpaymentrequest', 'allowpaymentrequest'))
  107. get('button').should(haveAttribute('formnovalidate', 'formnovalidate'))
  108. get('audio').should(haveAttribute('autoplay', 'autoplay'))
  109. get('audio').should(haveAttribute('controls', 'controls'))
  110. get('audio').should(haveAttribute('loop', 'loop'))
  111. get('audio').should(haveAttribute('muted', 'muted'))
  112. get('video').should(haveAttribute('playsinline', 'playsinline'))
  113. get('track').should(haveAttribute('default', 'default'))
  114. get('img').should(haveAttribute('ismap', 'ismap'))
  115. get('ol').should(haveAttribute('reversed', 'reversed'))
  116. get('#setToFalse').click()
  117. get('input:nth-of-type(1)').should(notHaveAttribute('disabled'))
  118. get('input:nth-of-type(2)').should(notHaveAttribute('checked'))
  119. get('input:nth-of-type(3)').should(notHaveAttribute('required'))
  120. get('input:nth-of-type(4)').should(notHaveAttribute('readonly'))
  121. get('details').should(notHaveAttribute('open'))
  122. get('select').should(notHaveAttribute('multiple'))
  123. get('option').should(notHaveAttribute('selected'))
  124. get('textarea').should(notHaveAttribute('autofocus'))
  125. get('dl').should(notHaveAttribute('itemscope'))
  126. get('form').should(notHaveAttribute('novalidate'))
  127. get('iframe').should(notHaveAttribute('allowfullscreen'))
  128. get('iframe').should(notHaveAttribute('allowpaymentrequest'))
  129. get('button').should(notHaveAttribute('formnovalidate'))
  130. get('audio').should(notHaveAttribute('autoplay'))
  131. get('audio').should(notHaveAttribute('controls'))
  132. get('audio').should(notHaveAttribute('loop'))
  133. get('audio').should(notHaveAttribute('muted'))
  134. get('video').should(notHaveAttribute('playsinline'))
  135. get('track').should(notHaveAttribute('default'))
  136. get('img').should(notHaveAttribute('ismap'))
  137. get('ol').should(notHaveAttribute('reversed'))
  138. get('script').should(notHaveAttribute('async'))
  139. get('script').should(notHaveAttribute('defer'))
  140. get('script').should(notHaveAttribute('nomodule'))
  141. }
  142. )
  143. test('boolean empty string attributes are not removed',
  144. html`
  145. <div x-data="{}">
  146. <input x-bind:disabled="''">
  147. </div>
  148. `,
  149. ({ get }) => get('input').should(haveAttribute('disabled', 'disabled'))
  150. )
  151. test('binding supports short syntax',
  152. html`
  153. <div x-data="{ foo: 'bar' }">
  154. <span :class="foo"></span>
  155. </div>
  156. `,
  157. ({ get }) => get('span').should(haveClasses(['bar']))
  158. )
  159. test('checkbox is unchecked by default',
  160. html`
  161. <div x-data="{foo: {bar: 'baz'}}">
  162. <input type="checkbox" x-bind:value="''"></input>
  163. <input type="checkbox" x-bind:value="'test'"></input>
  164. <input type="checkbox" x-bind:value="foo.bar"></input>
  165. <input type="checkbox" x-bind:value="0"></input>
  166. <input type="checkbox" x-bind:value="10"></input>
  167. </div>
  168. `,
  169. ({ get }) => {
  170. get('input:nth-of-type(1)').should(notBeChecked())
  171. get('input:nth-of-type(2)').should(notBeChecked())
  172. get('input:nth-of-type(3)').should(notBeChecked())
  173. get('input:nth-of-type(4)').should(notBeChecked())
  174. get('input:nth-of-type(5)').should(notBeChecked())
  175. }
  176. )
  177. test('radio is unchecked by default',
  178. html`
  179. <div x-data="{foo: {bar: 'baz'}}">
  180. <input type="radio" x-bind:value="''"></input>
  181. <input type="radio" x-bind:value="'test'"></input>
  182. <input type="radio" x-bind:value="foo.bar"></input>
  183. <input type="radio" x-bind:value="0"></input>
  184. <input type="radio" x-bind:value="10"></input>
  185. </div>
  186. `,
  187. ({ get }) => {
  188. get('input:nth-of-type(1)').should(notBeChecked())
  189. get('input:nth-of-type(2)').should(notBeChecked())
  190. get('input:nth-of-type(3)').should(notBeChecked())
  191. get('input:nth-of-type(4)').should(notBeChecked())
  192. get('input:nth-of-type(5)').should(notBeChecked())
  193. }
  194. )
  195. test('checkbox values are set correctly',
  196. html`
  197. <div x-data="{ stringValue: 'foo', trueValue: true, falseValue: false }">
  198. <input type="checkbox" name="stringCheckbox" :value="stringValue" />
  199. <input type="checkbox" name="trueCheckbox" :value="trueValue" />
  200. <input type="checkbox" name="falseCheckbox" :value="falseValue" />
  201. </div>
  202. `,
  203. ({ get }) => {
  204. get('input:nth-of-type(1)').should(haveValue('foo'))
  205. get('input:nth-of-type(2)').should(haveValue('on'))
  206. get('input:nth-of-type(3)').should(haveValue('on'))
  207. }
  208. )
  209. test('radio values are set correctly',
  210. html`
  211. <div x-data="{lists: [{id: 1}, {id: 8}], selectedListID: '8'}">
  212. <template x-for="list in lists" :key="list.id">
  213. <input x-model="selectedListID" type="radio" :value="list.id.toString()" :id="'list-' + list.id">
  214. </template>
  215. <input type="radio" id="list-test" value="test" x-model="selectedListID">
  216. </div>
  217. `,
  218. ({ get }) => {
  219. get('#list-1').should(haveValue('1'))
  220. get('#list-1').should(notBeChecked())
  221. get('#list-8').should(haveValue('8'))
  222. get('#list-8').should(beChecked())
  223. get('#list-test').should(haveValue('test'))
  224. get('#list-test').should(notBeChecked())
  225. }
  226. )
  227. test('.camel modifier correctly sets name of attribute',
  228. html`
  229. <div x-data>
  230. <svg x-bind:view-box.camel="'0 0 42 42'"></svg>
  231. </div>
  232. `,
  233. ({ get }) => get('svg').should(haveAttribute('viewBox', '0 0 42 42'))
  234. )
  235. test('attribute binding names can contain numbers',
  236. html`
  237. <svg x-data>
  238. <line x1="1" y1="2" :x2="3" x-bind:y2="4" />
  239. </svg>
  240. `,
  241. ({ get }) => {
  242. get('line').should(haveAttribute('x2', '3'))
  243. get('line').should(haveAttribute('y2', '4'))
  244. }
  245. )
  246. test('non-string and non-boolean attributes are cast to string when bound to checkbox',
  247. html`
  248. <div x-data="{ number: 100, zero: 0, bool: true, nullProp: null }">
  249. <input type="checkbox" id="number" :value="number">
  250. <input type="checkbox" id="zero" :value="zero">
  251. <input type="checkbox" id="boolean" :value="bool">
  252. <input type="checkbox" id="null" :value="nullProp">
  253. </div>
  254. `,
  255. ({ get }) => {
  256. get('input:nth-of-type(1)').should(haveValue('100'))
  257. get('input:nth-of-type(2)').should(haveValue('0'))
  258. get('input:nth-of-type(3)').should(haveValue('on'))
  259. get('input:nth-of-type(4)').should(haveValue('on'))
  260. }
  261. )
  262. test('can bind an object of directives',
  263. html`
  264. <script>
  265. window.modal = function () {
  266. return {
  267. foo: 'bar',
  268. trigger: {
  269. ['x-on:click']() { this.foo = 'baz' },
  270. },
  271. dialogue: {
  272. ['x-text']() { return this.foo },
  273. },
  274. }
  275. }
  276. </script>
  277. <div x-data="window.modal()">
  278. <button x-bind="trigger">Toggle</button>
  279. <span x-bind="dialogue">Modal Body</span>
  280. </div>
  281. `,
  282. ({ get }) => {
  283. get('span').should(haveText('bar'))
  284. get('button').click()
  285. get('span').should(haveText('baz'))
  286. }
  287. )
  288. test('x-bind object syntax supports normal HTML attributes',
  289. html`
  290. <span x-data x-bind="{ foo: 'bar' }"></span>
  291. `,
  292. ({ get }) => {
  293. get('span').should(haveAttribute('foo', 'bar'))
  294. }
  295. )
  296. test('x-bind object syntax supports x-for',
  297. html`
  298. <script>
  299. window.todos = () => { return {
  300. todos: ['foo', 'bar'],
  301. outputForExpression: {
  302. ['x-for']: 'todo in todos',
  303. }
  304. }}
  305. </script>
  306. <div x-data="window.todos()">
  307. <ul>
  308. <template x-bind="outputForExpression">
  309. <li x-text="todo"></li>
  310. </template>
  311. </ul>
  312. </div>
  313. `,
  314. ({ get }) => {
  315. get('li:nth-of-type(1)').should(haveText('foo'))
  316. get('li:nth-of-type(2)').should(haveText('bar'))
  317. }
  318. )
  319. test('x-bind object syntax syntax supports x-transition',
  320. html`
  321. <style>
  322. .transition { transition-property: background-color, border-color, color, fill, stroke; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
  323. .duration-100 { transition-duration: 100ms; }
  324. </style>
  325. <script>
  326. window.transitions = () => { return {
  327. show: true,
  328. outputClickExpression: {
  329. ['@click']() { this.show = false },
  330. ['x-text']() { return 'Click Me' },
  331. },
  332. outputTransitionExpression: {
  333. ['x-show']() { return this.show },
  334. ['x-transition:enter']: 'transition duration-100',
  335. ['x-transition:leave']: 'transition duration-100',
  336. },
  337. }}
  338. </script>
  339. <div x-data="transitions()">
  340. <button x-bind="outputClickExpression"></button>
  341. <span x-bind="outputTransitionExpression">thing</span>
  342. </div>
  343. `,
  344. ({ get }) => {
  345. get('span').should(beVisible())
  346. get('button').click()
  347. get('span').should(beVisible())
  348. get('span').should(beHidden())
  349. }
  350. )
  351. test('x-bind object syntax event handlers defined as functions receive the event object as their first argument',
  352. html`
  353. <script>
  354. window.data = () => { return {
  355. button: {
  356. ['@click']() {
  357. this.$refs.span.innerText = this.$el.id
  358. }
  359. }
  360. }}
  361. </script>
  362. <div x-data="window.data()">
  363. <button x-bind="button" id="bar">click me</button>
  364. <span x-ref="span">foo</span>
  365. </div>
  366. `,
  367. ({ get }) => {
  368. get('span').should(haveText('foo'))
  369. get('button').click()
  370. get('span').should(haveText('bar'))
  371. }
  372. )