morph.spec.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  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',
  114. [html`
  115. <ul>
  116. <li>foo<input></li>
  117. </ul>
  118. `],
  119. ({ get }, reload, window, document) => {
  120. let toHtml = html`
  121. <ul>
  122. <li>bar<input></li>
  123. <li>foo<input></li>
  124. </ul>
  125. `
  126. get('input').type('foo')
  127. get('ul').then(([el]) => window.Alpine.morph(el, toHtml))
  128. get('li').should(haveLength(2))
  129. get('li:nth-of-type(1)').should(haveText('bar'))
  130. get('li:nth-of-type(2)').should(haveText('foo'))
  131. get('li:nth-of-type(1) input').should(haveValue('foo'))
  132. get('li:nth-of-type(2) input').should(haveValue(''))
  133. },
  134. )
  135. test('can morph using lookahead',
  136. [html`
  137. <ul>
  138. <li>foo<input></li>
  139. </ul>
  140. `],
  141. ({ get }, reload, window, document) => {
  142. let toHtml = html`
  143. <ul>
  144. <li>bar<input></li>
  145. <li>baz<input></li>
  146. <li>foo<input></li>
  147. </ul>
  148. `
  149. get('input').type('foo')
  150. get('ul').then(([el]) => window.Alpine.morph(el, toHtml, {lookahead: true}))
  151. get('li').should(haveLength(3))
  152. get('li:nth-of-type(1)').should(haveText('bar'))
  153. get('li:nth-of-type(2)').should(haveText('baz'))
  154. get('li:nth-of-type(3)').should(haveText('foo'))
  155. get('li:nth-of-type(1) input').should(haveValue(''))
  156. get('li:nth-of-type(2) input').should(haveValue(''))
  157. get('li:nth-of-type(3) input').should(haveValue('foo'))
  158. },
  159. )
  160. test('can morph using keys',
  161. [html`
  162. <ul>
  163. <li key="1">foo<input></li>
  164. </ul>
  165. `],
  166. ({ get }, reload, window, document) => {
  167. let toHtml = html`
  168. <ul>
  169. <li key="2">bar<input></li>
  170. <li key="3">baz<input></li>
  171. <li key="1">foo<input></li>
  172. </ul>
  173. `
  174. get('input').type('foo')
  175. get('ul').then(([el]) => window.Alpine.morph(el, toHtml))
  176. get('li').should(haveLength(3))
  177. get('li:nth-of-type(1)').should(haveText('bar'))
  178. get('li:nth-of-type(2)').should(haveText('baz'))
  179. get('li:nth-of-type(3)').should(haveText('foo'))
  180. get('li:nth-of-type(1) input').should(haveValue(''))
  181. get('li:nth-of-type(2) input').should(haveValue(''))
  182. get('li:nth-of-type(3) input').should(haveValue('foo'))
  183. },
  184. )
  185. test('can morph using a custom key function',
  186. [html`
  187. <ul>
  188. <li data-key="1">foo<input></li>
  189. </ul>
  190. `],
  191. ({ get }, reload, window, document) => {
  192. let toHtml = html`
  193. <ul>
  194. <li data-key="2">bar<input></li>
  195. <li data-key="3">baz<input></li>
  196. <li data-key="1">foo<input></li>
  197. </ul>
  198. `
  199. get('input').type('foo')
  200. get('ul').then(([el]) => window.Alpine.morph(el, toHtml, {key(el) {return el.dataset.key}}))
  201. get('li').should(haveLength(3))
  202. get('li:nth-of-type(1)').should(haveText('bar'))
  203. get('li:nth-of-type(2)').should(haveText('baz'))
  204. get('li:nth-of-type(3)').should(haveText('foo'))
  205. get('li:nth-of-type(1) input').should(haveValue(''))
  206. get('li:nth-of-type(2) input').should(haveValue(''))
  207. get('li:nth-of-type(3) input').should(haveValue('foo'))
  208. },
  209. )
  210. test('can morph using keys with existing key to be moved up',
  211. [html`
  212. <ul>
  213. <li key="1">foo<input></li>
  214. <li key="2">bar<input></li>
  215. <li key="3">baz<input></li>
  216. </ul>
  217. `],
  218. ({ get }, reload, window, document) => {
  219. let toHtml = html`
  220. <ul>
  221. <li key="1">foo<input></li>
  222. <li key="3">baz<input></li>
  223. </ul>
  224. `
  225. get('li:nth-of-type(1) input').type('foo')
  226. get('li:nth-of-type(3) input').type('baz')
  227. get('ul').then(([el]) => window.Alpine.morph(el, toHtml))
  228. get('li').should(haveLength(2))
  229. get('li:nth-of-type(1)').should(haveText('foo'))
  230. get('li:nth-of-type(2)').should(haveText('baz'))
  231. get('li:nth-of-type(1) input').should(haveValue('foo'))
  232. get('li:nth-of-type(2) input').should(haveValue('baz'))
  233. },
  234. )
  235. test('can morph text nodes',
  236. [html`<h2>Foo <br> Bar</h2>`],
  237. ({ get }, reload, window, document) => {
  238. let toHtml = html`<h2>Foo <br> Baz</h2>`
  239. get('h2').then(([el]) => window.Alpine.morph(el, toHtml))
  240. get('h2').should(haveHtml('Foo <br> Baz'))
  241. },
  242. )
  243. test('can morph with added element before and siblings are different',
  244. [html`
  245. <button>
  246. <div>
  247. <div>second</div>
  248. <div data="false">third</div>
  249. </div>
  250. </button>
  251. `],
  252. ({ get }, reload, window, document) => {
  253. let toHtml = html`
  254. <button>
  255. <div>first</div>
  256. <div>
  257. <div>second</div>
  258. <div data="true">third</div>
  259. </div>
  260. </button>
  261. `
  262. get('button').then(([el]) => window.Alpine.morph(el, toHtml))
  263. get('button > div').should(haveLength(2))
  264. get('button > div:nth-of-type(1)').should(haveText('first'))
  265. get('button > div:nth-of-type(2)').should(haveHtml(`
  266. <div>second</div>
  267. <div data="true">third</div>
  268. `))
  269. },
  270. )
  271. test('can morph using different keys',
  272. [html`
  273. <ul>
  274. <li key="1">foo</li>
  275. </ul>
  276. `],
  277. ({ get }, reload, window, document) => {
  278. let toHtml = html`
  279. <ul>
  280. <li key="2">bar</li>
  281. </ul>
  282. `
  283. get('ul').then(([el]) => window.Alpine.morph(el, toHtml))
  284. get('li').should(haveLength(1))
  285. get('li:nth-of-type(1)').should(haveText('bar'))
  286. get('li:nth-of-type(1)').should(haveAttribute('key', '2'))
  287. },
  288. )
  289. test('can morph elements with dynamic ids',
  290. [html`
  291. <ul>
  292. <li x-data x-bind:id="'1'" >foo<input></li>
  293. </ul>
  294. `],
  295. ({ get }, reload, window, document) => {
  296. let toHtml = html`
  297. <ul>
  298. <li x-data x-bind:id="'1'" >foo<input></li>
  299. </ul>
  300. `
  301. get('input').type('foo')
  302. get('ul').then(([el]) => window.Alpine.morph(el, toHtml, {
  303. key(el) { return el.id }
  304. }))
  305. get('li:nth-of-type(1) input').should(haveValue('foo'))
  306. },
  307. )
  308. test('can morph different inline nodes',
  309. [html`
  310. <div id="from">
  311. Hello <span>World</span>
  312. </div>
  313. `],
  314. ({ get }, reload, window, document) => {
  315. let toHtml = html`
  316. <div id="to">
  317. Welcome <b>Person</b>!
  318. </div>
  319. `
  320. get('div').then(([el]) => window.Alpine.morph(el, toHtml))
  321. get('div').should(haveHtml('\n Welcome <b>Person</b>!\n '))
  322. },
  323. )
  324. test('can morph multiple nodes',
  325. [html`
  326. <div x-data>
  327. <p></p>
  328. <p></p>
  329. </div>
  330. `],
  331. ({ get }, reload, window, document) => {
  332. let paragraphs = document.querySelectorAll('p')
  333. window.Alpine.morph(paragraphs[0], '<p>1</p')
  334. window.Alpine.morph(paragraphs[1], '<p>2</p')
  335. get('p:nth-of-type(1)').should(haveText('1'))
  336. get('p:nth-of-type(2)').should(haveText('2'))
  337. },
  338. )
  339. test('can morph table tr',
  340. [html`
  341. <table>
  342. <tr><td>1</td></tr>
  343. </table>
  344. `],
  345. ({ get }, reload, window, document) => {
  346. let tr = document.querySelector('tr')
  347. window.Alpine.morph(tr, '<tr><td>2</td></tr>')
  348. get('td').should(haveText('2'))
  349. },
  350. )
  351. test('can morph with conditional markers',
  352. [html`
  353. <main>
  354. <!--[if BLOCK]><![endif]-->
  355. <div>foo<input></div>
  356. <!--[if ENDBLOCK]><![endif]-->
  357. <div>bar<input></div>
  358. </main>
  359. `],
  360. ({ get }, reload, window, document) => {
  361. let toHtml = html`
  362. <main>
  363. <!--[if BLOCK]><![endif]-->
  364. <div>foo<input></div>
  365. <div>baz<input></div>
  366. <!--[if ENDBLOCK]><![endif]-->
  367. <div>bar<input></div>
  368. </main>
  369. `
  370. get('div:nth-of-type(1) input').type('foo')
  371. get('div:nth-of-type(2) input').type('bar')
  372. get('main').then(([el]) => window.Alpine.morph(el, toHtml))
  373. get('div:nth-of-type(1) input').should(haveValue('foo'))
  374. get('div:nth-of-type(2) input').should(haveValue(''))
  375. get('div:nth-of-type(3) input').should(haveValue('bar'))
  376. },
  377. )
  378. test('can morph with flat-nested conditional markers',
  379. [html`
  380. <main>
  381. <!--[if BLOCK]><![endif]-->
  382. <div>foo<input></div>
  383. <!--[if BLOCK]><![endif]-->
  384. <!--[if ENDBLOCK]><![endif]-->
  385. <!--[if ENDBLOCK]><![endif]-->
  386. <div>bar<input></div>
  387. </main>
  388. `],
  389. ({ get }, reload, window, document) => {
  390. let toHtml = html`
  391. <main>
  392. <!--[if BLOCK]><![endif]-->
  393. <div>foo<input></div>
  394. <!--[if BLOCK]><![endif]-->
  395. <!--[if ENDBLOCK]><![endif]-->
  396. <div>baz<input></div>
  397. <!--[if ENDBLOCK]><![endif]-->
  398. <div>bar<input></div>
  399. </main>
  400. `
  401. get('div:nth-of-type(1) input').type('foo')
  402. get('div:nth-of-type(2) input').type('bar')
  403. get('main').then(([el]) => window.Alpine.morph(el, toHtml))
  404. get('div:nth-of-type(1) input').should(haveValue('foo'))
  405. get('div:nth-of-type(2) input').should(haveValue(''))
  406. get('div:nth-of-type(3) input').should(haveValue('bar'))
  407. },
  408. )
  409. // '@event' handlers cannot be assigned directly on the element without Alpine's internl monkey patching...
  410. test('can morph @event handlers', [
  411. html`
  412. <div x-data="{ foo: 'bar' }">
  413. <button x-text="foo"></button>
  414. </div>
  415. `],
  416. ({ get, click }, reload, window, document) => {
  417. let toHtml = html`
  418. <button @click="foo = 'buzz'" x-text="foo"></button>
  419. `;
  420. get('button').should(haveText('bar'));
  421. get('button').then(([el]) => window.Alpine.morph(el, toHtml));
  422. get('button').click();
  423. get('button').should(haveText('buzz'));
  424. }
  425. );
  426. test('can morph menu',
  427. [html`
  428. <main x-data>
  429. <article x-menu>
  430. <button data-trigger x-menu:button x-text="'ready'"></button>
  431. <div x-menu:items>
  432. <button x-menu:item href="#edit">
  433. Edit
  434. <input>
  435. </button>
  436. </div>
  437. </article>
  438. </main>
  439. `],
  440. ({ get }, reload, window, document) => {
  441. let toHtml = html`
  442. <main x-data>
  443. <article x-menu>
  444. <button data-trigger x-menu:button x-text="'ready'"></button>
  445. <div x-menu:items>
  446. <button x-menu:item href="#edit">
  447. Edit
  448. <input>
  449. </button>
  450. </div>
  451. </article>
  452. </main>
  453. `
  454. get('[data-trigger]').should(haveText('ready'));
  455. get('button[data-trigger').click()
  456. get('input').type('foo')
  457. get('main').then(([el]) => window.Alpine.morph(el, toHtml, {
  458. key(el) { return el.id }
  459. }))
  460. get('input').should(haveValue('foo'))
  461. },
  462. )