morph.spec.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654
  1. import { haveAttribute, haveLength, haveText, haveValue, haveHtml, html, test } from '../../utils'
  2. test('can morph components and preserve Alpine state',
  3. [html`
  4. <div x-data="{ foo: 'bar' }">
  5. <button @click="foo = 'baz'">Change Foo</button>
  6. <span x-text="foo"></span>
  7. </div>
  8. `],
  9. ({ get }, reload, window, document) => {
  10. let toHtml = document.querySelector('div').outerHTML
  11. get('span').should(haveText('bar'))
  12. get('button').click()
  13. get('span').should(haveText('baz'))
  14. get('div').then(([el]) => window.Alpine.morph(el, toHtml))
  15. get('span').should(haveText('baz'))
  16. },
  17. )
  18. test('morphing target uses outer Alpine scope',
  19. [html`
  20. <article x-data="{ foo: 'bar' }">
  21. <div>
  22. <button @click="foo = 'baz'">Change Foo</button>
  23. <span x-text="foo"></span>
  24. </div>
  25. </article>
  26. `],
  27. ({ get }, reload, window, document) => {
  28. let toHtml = document.querySelector('div').outerHTML
  29. get('span').should(haveText('bar'))
  30. get('button').click()
  31. get('span').should(haveText('baz'))
  32. get('div').then(([el]) => window.Alpine.morph(el, toHtml))
  33. get('span').should(haveText('baz'))
  34. },
  35. )
  36. test('can morph with HTML change and preserve Alpine state',
  37. [html`
  38. <div x-data="{ foo: 'bar' }">
  39. <button @click="foo = 'baz'">Change Foo</button>
  40. <span x-text="foo"></span>
  41. </div>
  42. `],
  43. ({ get }, reload, window, document) => {
  44. let toHtml = document.querySelector('div').outerHTML.replace('Change Foo', 'Changed Foo')
  45. get('span').should(haveText('bar'))
  46. get('button').click()
  47. get('span').should(haveText('baz'))
  48. get('button').should(haveText('Change Foo'))
  49. get('div').then(([el]) => window.Alpine.morph(el, toHtml))
  50. get('span').should(haveText('baz'))
  51. get('button').should(haveText('Changed Foo'))
  52. },
  53. )
  54. test('morphing an element with multiple nested Alpine components preserves scope',
  55. [html`
  56. <div x-data="{ foo: 'bar' }">
  57. <button @click="foo = 'baz'">Change Foo</button>
  58. <span x-text="foo"></span>
  59. <div x-data="{ bob: 'lob' }">
  60. <a href="#" @click.prevent="bob = 'law'">Change Bob</a>
  61. <h1 x-text="bob"></h1>
  62. </div>
  63. </div>
  64. `],
  65. ({ get }, reload, window, document) => {
  66. let toHtml = document.querySelector('div').outerHTML
  67. get('span').should(haveText('bar'))
  68. get('h1').should(haveText('lob'))
  69. get('button').click()
  70. get('a').click()
  71. get('span').should(haveText('baz'))
  72. get('h1').should(haveText('law'))
  73. get('div').then(([el]) => window.Alpine.morph(el, toHtml))
  74. get('span').should(haveText('baz'))
  75. get('h1').should(haveText('law'))
  76. },
  77. )
  78. test('can morph teleports',
  79. [html`
  80. <div x-data="{ count: 1 }" id="a">
  81. <button @click="count++">Inc</button>
  82. <template x-teleport="#b">
  83. <div>
  84. <h1 x-text="count"></h1>
  85. <h2>hey</h2>
  86. </div>
  87. </template>
  88. </div>
  89. <div id="b"></div>
  90. `],
  91. ({ get }, reload, window, document) => {
  92. let toHtml = html`
  93. <div x-data="{ count: 1 }" id="a">
  94. <button @click="count++">Inc</button>
  95. <template x-teleport="#b">
  96. <div>
  97. <h1 x-text="count"></h1>
  98. <h2>there</h2>
  99. </div>
  100. </template>
  101. </div>
  102. `
  103. get('h1').should(haveText('1'))
  104. get('h2').should(haveText('hey'))
  105. get('button').click()
  106. get('h1').should(haveText('2'))
  107. get('h2').should(haveText('hey'))
  108. get('div#a').then(([el]) => window.Alpine.morph(el, toHtml))
  109. get('h1').should(haveText('2'))
  110. get('h2').should(haveText('there'))
  111. },
  112. )
  113. test('can morph teleports in different places with IDs',
  114. [html`
  115. <div x-data="{ count: 1 }" id="a">
  116. <button @click="count++">Inc</button>
  117. <template x-teleport="#b" id="template">
  118. <div>
  119. <h1 x-text="count"></h1>
  120. <h2>hey</h2>
  121. </div>
  122. </template>
  123. <div>moving placeholder</div>
  124. </div>
  125. <div id="b"></div>
  126. `],
  127. ({ get }, reload, window, document) => {
  128. let toHtml = html`
  129. <div x-data="{ count: 1 }" id="a">
  130. <button @click="count++">Inc</button>
  131. <div>moving placeholder</div>
  132. <template x-teleport="#b" id="template">
  133. <div>
  134. <h1 x-text="count"></h1>
  135. <h2>there</h2>
  136. </div>
  137. </template>
  138. </div>
  139. `
  140. get('h1').should(haveText('1'))
  141. get('h2').should(haveText('hey'))
  142. get('button').click()
  143. get('h1').should(haveText('2'))
  144. get('h2').should(haveText('hey'))
  145. get('div#a').then(([el]) => window.Alpine.morph(el, toHtml))
  146. get('h1').should(haveText('2'))
  147. get('h2').should(haveText('there'))
  148. },
  149. )
  150. test('can morph',
  151. [html`
  152. <ul>
  153. <li>foo<input></li>
  154. </ul>
  155. `],
  156. ({ get }, reload, window, document) => {
  157. let toHtml = html`
  158. <ul>
  159. <li>bar<input></li>
  160. <li>foo<input></li>
  161. </ul>
  162. `
  163. get('input').type('foo')
  164. get('ul').then(([el]) => window.Alpine.morph(el, toHtml))
  165. get('li').should(haveLength(2))
  166. get('li:nth-of-type(1)').should(haveText('bar'))
  167. get('li:nth-of-type(2)').should(haveText('foo'))
  168. get('li:nth-of-type(1) input').should(haveValue('foo'))
  169. get('li:nth-of-type(2) input').should(haveValue(''))
  170. },
  171. )
  172. test('can morph using lookahead',
  173. [html`
  174. <ul>
  175. <li>foo<input></li>
  176. </ul>
  177. `],
  178. ({ get }, reload, window, document) => {
  179. let toHtml = html`
  180. <ul>
  181. <li>bar<input></li>
  182. <li>baz<input></li>
  183. <li>foo<input></li>
  184. </ul>
  185. `
  186. get('input').type('foo')
  187. get('ul').then(([el]) => window.Alpine.morph(el, toHtml, {lookahead: true}))
  188. get('li').should(haveLength(3))
  189. get('li:nth-of-type(1)').should(haveText('bar'))
  190. get('li:nth-of-type(2)').should(haveText('baz'))
  191. get('li:nth-of-type(3)').should(haveText('foo'))
  192. get('li:nth-of-type(1) input').should(haveValue(''))
  193. get('li:nth-of-type(2) input').should(haveValue(''))
  194. get('li:nth-of-type(3) input').should(haveValue('foo'))
  195. },
  196. )
  197. test('can morph using keys',
  198. [html`
  199. <ul>
  200. <li key="1">foo<input></li>
  201. </ul>
  202. `],
  203. ({ get }, reload, window, document) => {
  204. let toHtml = html`
  205. <ul>
  206. <li key="2">bar<input></li>
  207. <li key="3">baz<input></li>
  208. <li key="1">foo<input></li>
  209. </ul>
  210. `
  211. get('input').type('foo')
  212. get('ul').then(([el]) => window.Alpine.morph(el, toHtml))
  213. get('li').should(haveLength(3))
  214. get('li:nth-of-type(1)').should(haveText('bar'))
  215. get('li:nth-of-type(2)').should(haveText('baz'))
  216. get('li:nth-of-type(3)').should(haveText('foo'))
  217. get('li:nth-of-type(1) input').should(haveValue(''))
  218. get('li:nth-of-type(2) input').should(haveValue(''))
  219. get('li:nth-of-type(3) input').should(haveValue('foo'))
  220. },
  221. )
  222. test('can morph using a custom key function',
  223. [html`
  224. <ul>
  225. <li data-key="1">foo<input></li>
  226. </ul>
  227. `],
  228. ({ get }, reload, window, document) => {
  229. let toHtml = html`
  230. <ul>
  231. <li data-key="2">bar<input></li>
  232. <li data-key="3">baz<input></li>
  233. <li data-key="1">foo<input></li>
  234. </ul>
  235. `
  236. get('input').type('foo')
  237. get('ul').then(([el]) => window.Alpine.morph(el, toHtml, {key(el) {return el.dataset.key}}))
  238. get('li').should(haveLength(3))
  239. get('li:nth-of-type(1)').should(haveText('bar'))
  240. get('li:nth-of-type(2)').should(haveText('baz'))
  241. get('li:nth-of-type(3)').should(haveText('foo'))
  242. get('li:nth-of-type(1) input').should(haveValue(''))
  243. get('li:nth-of-type(2) input').should(haveValue(''))
  244. get('li:nth-of-type(3) input').should(haveValue('foo'))
  245. },
  246. )
  247. test('can morph using keys with existing key to be moved up',
  248. [html`
  249. <ul>
  250. <li key="1">foo<input></li>
  251. <li key="2">bar<input></li>
  252. <li key="3">baz<input></li>
  253. </ul>
  254. `],
  255. ({ get }, reload, window, document) => {
  256. let toHtml = html`
  257. <ul>
  258. <li key="1">foo<input></li>
  259. <li key="3">baz<input></li>
  260. </ul>
  261. `
  262. get('li:nth-of-type(1) input').type('foo')
  263. get('li:nth-of-type(3) input').type('baz')
  264. get('ul').then(([el]) => window.Alpine.morph(el, toHtml))
  265. get('li').should(haveLength(2))
  266. get('li:nth-of-type(1)').should(haveText('foo'))
  267. get('li:nth-of-type(2)').should(haveText('baz'))
  268. get('li:nth-of-type(1) input').should(haveValue('foo'))
  269. get('li:nth-of-type(2) input').should(haveValue('baz'))
  270. },
  271. )
  272. test('can morph text nodes',
  273. [html`<h2>Foo <br> Bar</h2>`],
  274. ({ get }, reload, window, document) => {
  275. let toHtml = html`<h2>Foo <br> Baz</h2>`
  276. get('h2').then(([el]) => window.Alpine.morph(el, toHtml))
  277. get('h2').should(haveHtml('Foo <br> Baz'))
  278. },
  279. )
  280. test('can morph with added element before and siblings are different',
  281. [html`
  282. <button>
  283. <div>
  284. <div>second</div>
  285. <div data="false">third</div>
  286. </div>
  287. </button>
  288. `],
  289. ({ get }, reload, window, document) => {
  290. let toHtml = html`
  291. <button>
  292. <div>first</div>
  293. <div>
  294. <div>second</div>
  295. <div data="true">third</div>
  296. </div>
  297. </button>
  298. `
  299. get('button').then(([el]) => window.Alpine.morph(el, toHtml))
  300. get('button > div').should(haveLength(2))
  301. get('button > div:nth-of-type(1)').should(haveText('first'))
  302. get('button > div:nth-of-type(2)').should(haveHtml(`
  303. <div>second</div>
  304. <div data="true">third</div>
  305. `))
  306. },
  307. )
  308. test('can morph using different keys',
  309. [html`
  310. <ul>
  311. <li key="1">foo</li>
  312. </ul>
  313. `],
  314. ({ get }, reload, window, document) => {
  315. let toHtml = html`
  316. <ul>
  317. <li key="2">bar</li>
  318. </ul>
  319. `
  320. get('ul').then(([el]) => window.Alpine.morph(el, toHtml))
  321. get('li').should(haveLength(1))
  322. get('li:nth-of-type(1)').should(haveText('bar'))
  323. get('li:nth-of-type(1)').should(haveAttribute('key', '2'))
  324. },
  325. )
  326. test('can morph elements with dynamic ids',
  327. [html`
  328. <ul>
  329. <li x-data x-bind:id="'1'" >foo<input></li>
  330. </ul>
  331. `],
  332. ({ get }, reload, window, document) => {
  333. let toHtml = html`
  334. <ul>
  335. <li x-data x-bind:id="'1'" >foo<input></li>
  336. </ul>
  337. `
  338. get('input').type('foo')
  339. get('ul').then(([el]) => window.Alpine.morph(el, toHtml, {
  340. key(el) { return el.id }
  341. }))
  342. get('li:nth-of-type(1) input').should(haveValue('foo'))
  343. },
  344. )
  345. test('can morph different inline nodes',
  346. [html`
  347. <div id="from">
  348. Hello <span>World</span>
  349. </div>
  350. `],
  351. ({ get }, reload, window, document) => {
  352. let toHtml = html`
  353. <div id="to">
  354. Welcome <b>Person</b>!
  355. </div>
  356. `
  357. get('div').then(([el]) => window.Alpine.morph(el, toHtml))
  358. get('div').should(haveHtml('\n Welcome <b>Person</b>!\n '))
  359. },
  360. )
  361. test('can morph multiple nodes',
  362. [html`
  363. <div x-data>
  364. <p></p>
  365. <p></p>
  366. </div>
  367. `],
  368. ({ get }, reload, window, document) => {
  369. let paragraphs = document.querySelectorAll('p')
  370. window.Alpine.morph(paragraphs[0], '<p>1</p')
  371. window.Alpine.morph(paragraphs[1], '<p>2</p')
  372. get('p:nth-of-type(1)').should(haveText('1'))
  373. get('p:nth-of-type(2)').should(haveText('2'))
  374. },
  375. )
  376. test('can morph table tr',
  377. [html`
  378. <table>
  379. <tr><td>1</td></tr>
  380. </table>
  381. `],
  382. ({ get }, reload, window, document) => {
  383. let tr = document.querySelector('tr')
  384. window.Alpine.morph(tr, '<tr><td>2</td></tr>')
  385. get('td').should(haveText('2'))
  386. },
  387. )
  388. test('can morph with conditional markers',
  389. [html`
  390. <main>
  391. <!--[if BLOCK]><![endif]-->
  392. <div>foo<input></div>
  393. <!--[if ENDBLOCK]><![endif]-->
  394. <div>bar<input></div>
  395. </main>
  396. `],
  397. ({ get }, reload, window, document) => {
  398. let toHtml = html`
  399. <main>
  400. <!--[if BLOCK]><![endif]-->
  401. <div>foo<input></div>
  402. <div>baz<input></div>
  403. <!--[if ENDBLOCK]><![endif]-->
  404. <div>bar<input></div>
  405. </main>
  406. `
  407. get('div:nth-of-type(1) input').type('foo')
  408. get('div:nth-of-type(2) input').type('bar')
  409. get('main').then(([el]) => window.Alpine.morph(el, toHtml))
  410. get('div:nth-of-type(1) input').should(haveValue('foo'))
  411. get('div:nth-of-type(2) input').should(haveValue(''))
  412. get('div:nth-of-type(3) input').should(haveValue('bar'))
  413. },
  414. )
  415. test('can morph with flat-nested conditional markers',
  416. [html`
  417. <main>
  418. <!--[if BLOCK]><![endif]-->
  419. <div>foo<input></div>
  420. <!--[if BLOCK]><![endif]-->
  421. <!--[if ENDBLOCK]><![endif]-->
  422. <!--[if ENDBLOCK]><![endif]-->
  423. <div>bar<input></div>
  424. </main>
  425. `],
  426. ({ get }, reload, window, document) => {
  427. let toHtml = html`
  428. <main>
  429. <!--[if BLOCK]><![endif]-->
  430. <div>foo<input></div>
  431. <!--[if BLOCK]><![endif]-->
  432. <!--[if ENDBLOCK]><![endif]-->
  433. <div>baz<input></div>
  434. <!--[if ENDBLOCK]><![endif]-->
  435. <div>bar<input></div>
  436. </main>
  437. `
  438. get('div:nth-of-type(1) input').type('foo')
  439. get('div:nth-of-type(2) input').type('bar')
  440. get('main').then(([el]) => window.Alpine.morph(el, toHtml))
  441. get('div:nth-of-type(1) input').should(haveValue('foo'))
  442. get('div:nth-of-type(2) input').should(haveValue(''))
  443. get('div:nth-of-type(3) input').should(haveValue('bar'))
  444. },
  445. )
  446. // '@event' handlers cannot be assigned directly on the element without Alpine's internl monkey patching...
  447. test('can morph @event handlers', [
  448. html`
  449. <div x-data="{ foo: 'bar' }">
  450. <button x-text="foo"></button>
  451. </div>
  452. `],
  453. ({ get, click }, reload, window, document) => {
  454. let toHtml = html`
  455. <button @click="foo = 'buzz'" x-text="foo"></button>
  456. `;
  457. get('button').should(haveText('bar'));
  458. get('button').then(([el]) => window.Alpine.morph(el, toHtml));
  459. get('button').click();
  460. get('button').should(haveText('buzz'));
  461. }
  462. );
  463. test('can morph menu',
  464. [html`
  465. <main x-data>
  466. <article x-menu>
  467. <button data-trigger x-menu:button x-text="'ready'"></button>
  468. <div x-menu:items>
  469. <button x-menu:item href="#edit">
  470. Edit
  471. <input>
  472. </button>
  473. </div>
  474. </article>
  475. </main>
  476. `],
  477. ({ get }, reload, window, document) => {
  478. let toHtml = html`
  479. <main x-data>
  480. <article x-menu>
  481. <button data-trigger x-menu:button x-text="'ready'"></button>
  482. <div x-menu:items>
  483. <button x-menu:item href="#edit">
  484. Edit
  485. <input>
  486. </button>
  487. </div>
  488. </article>
  489. </main>
  490. `
  491. get('[data-trigger]').should(haveText('ready'));
  492. get('button[data-trigger').click()
  493. get('input').type('foo')
  494. get('main').then(([el]) => window.Alpine.morph(el, toHtml, {
  495. key(el) { return el.id }
  496. }))
  497. get('input').should(haveValue('foo'))
  498. },
  499. )
  500. test('can morph teleports with x-for',
  501. [html`
  502. <main x-data>
  503. <template x-teleport="body">
  504. <article>
  505. <template x-for="item in 3" :key="item">
  506. <span x-text="item"></span>
  507. </template>
  508. </article>
  509. </template>
  510. <button x-data="{ count: 1 }" x-text="count" x-on:click="count++" type="button"></button>
  511. </main>
  512. `],
  513. ({ get }, reload, window, document) => {
  514. let toHtml = html`
  515. <main x-data>
  516. <template x-teleport="body">
  517. <article>
  518. <template x-for="item in 3" :key="item">
  519. <span x-text="item"></span>
  520. </template>
  521. </article>
  522. </template>
  523. <button x-data="{ count: 1 }" x-text="count" x-on:click="count++" type="button"></button>
  524. </main>
  525. `
  526. get('button').should(haveText('1'));
  527. get('button').click()
  528. get('button').should(haveText('2'));
  529. get('main').then(([el]) => window.Alpine.morph(el, toHtml));
  530. get('button').should(haveText('2'));
  531. get('button').click()
  532. get('button').should(haveText('3'));
  533. },
  534. )
  535. test('can morph teleports with root-level state',
  536. [html`
  537. <main x-data>
  538. <template x-teleport="body">
  539. <div x-data="{ foo: 'bar' }">
  540. <h1 x-text="foo"></h1>
  541. </div>
  542. </template>
  543. </main>
  544. `],
  545. ({ get }, reload, window, document) => {
  546. let toHtml = html`
  547. <main x-data>
  548. <template x-teleport="body">
  549. <div x-data="{ foo: 'bar' }">
  550. <h1 x-text="foo"></h1>
  551. </div>
  552. </template>
  553. </main>
  554. `
  555. get('h1').should(haveText('bar'));
  556. get('main').then(([el]) => window.Alpine.morph(el, toHtml));
  557. get('h1').should(haveText('bar'));
  558. },
  559. )