morph.spec.js 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063
  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. )
  560. test('can use morphBetween with comment markers',
  561. [html`
  562. <div>
  563. <h2>Header</h2>
  564. <!--start-->
  565. <p>Original content</p>
  566. <!--end-->
  567. <h2>Footer</h2>
  568. </div>
  569. `],
  570. ({ get }, reload, window, document) => {
  571. // Find the comment markers
  572. let startMarker, endMarker;
  573. const walker = document.createTreeWalker(
  574. document.body,
  575. NodeFilter.SHOW_COMMENT,
  576. null,
  577. false
  578. );
  579. let node;
  580. while (node = walker.nextNode()) {
  581. if (node.textContent === 'start') startMarker = node;
  582. if (node.textContent === 'end') endMarker = node;
  583. }
  584. window.Alpine.morphBetween(startMarker, endMarker, '<p>New content</p><p>More content</p>')
  585. get('h2:nth-of-type(1)').should(haveText('Header'))
  586. get('h2:nth-of-type(2)').should(haveText('Footer'))
  587. get('p').should(haveLength(2))
  588. get('p:nth-of-type(1)').should(haveText('New content'))
  589. get('p:nth-of-type(2)').should(haveText('More content'))
  590. },
  591. )
  592. test('morphBetween preserves Alpine state',
  593. [html`
  594. <div x-data="{ count: 1 }">
  595. <button @click="count++">Inc</button>
  596. <!--morph-start-->
  597. <p x-text="count"></p>
  598. <input x-model="count">
  599. <!--morph-end-->
  600. <span>Static content</span>
  601. </div>
  602. `],
  603. ({ get }, reload, window, document) => {
  604. // Find markers
  605. let startMarker, endMarker;
  606. const walker = document.createTreeWalker(
  607. document.body,
  608. NodeFilter.SHOW_COMMENT,
  609. null,
  610. false
  611. );
  612. let node;
  613. while (node = walker.nextNode()) {
  614. if (node.textContent === 'morph-start') startMarker = node;
  615. if (node.textContent === 'morph-end') endMarker = node;
  616. }
  617. get('p').should(haveText('1'))
  618. get('button').click()
  619. get('p').should(haveText('2'))
  620. window.Alpine.morphBetween(startMarker, endMarker, `
  621. <p x-text="count"></p>
  622. <article>New element</article>
  623. <input x-model="count">
  624. `)
  625. get('p').should(haveText('2'))
  626. get('article').should(haveText('New element'))
  627. get('input').should(haveValue('2'))
  628. get('input').clear().type('5')
  629. get('p').should(haveText('5'))
  630. },
  631. )
  632. test('morphBetween with keyed elements',
  633. [html`
  634. <ul>
  635. <!--items-start-->
  636. <li key="1">foo<input></li>
  637. <li key="2">bar<input></li>
  638. <!--items-end-->
  639. </ul>
  640. `],
  641. ({ get }, reload, window, document) => {
  642. // Find markers
  643. let startMarker, endMarker;
  644. const walker = document.createTreeWalker(
  645. document.body,
  646. NodeFilter.SHOW_COMMENT,
  647. null,
  648. false
  649. );
  650. let node;
  651. while (node = walker.nextNode()) {
  652. if (node.textContent === 'items-start') startMarker = node;
  653. if (node.textContent === 'items-end') endMarker = node;
  654. }
  655. get('li:nth-of-type(1) input').type('first')
  656. get('li:nth-of-type(2) input').type('second')
  657. get('ul').then(([el]) => window.Alpine.morphBetween(startMarker, endMarker, `
  658. <li key="3">baz<input></li>
  659. <li key="1">foo<input></li>
  660. <li key="2">bar<input></li>
  661. `, { key(el) { return el.getAttribute('key') } }))
  662. get('li').should(haveLength(3))
  663. get('li:nth-of-type(1)').should(haveText('baz'))
  664. get('li:nth-of-type(2)').should(haveText('foo'))
  665. get('li:nth-of-type(3)').should(haveText('bar'))
  666. // Need to verify by the key attribute since the elements have been reordered
  667. get('li[key="1"] input').should(haveValue('first'))
  668. get('li[key="2"] input').should(haveValue('second'))
  669. get('li[key="3"] input').should(haveValue(''))
  670. },
  671. )
  672. test('morphBetween with custom key function',
  673. [html`
  674. <div>
  675. <!--start-->
  676. <div data-id="a">Item A<input></div>
  677. <div data-id="b">Item B<input></div>
  678. <!--end-->
  679. </div>
  680. `],
  681. ({ get }, reload, window, document) => {
  682. // Find markers
  683. let startMarker, endMarker;
  684. const walker = document.createTreeWalker(
  685. document.body,
  686. NodeFilter.SHOW_COMMENT,
  687. null,
  688. false
  689. );
  690. let node;
  691. while (node = walker.nextNode()) {
  692. if (node.textContent === 'start') startMarker = node;
  693. if (node.textContent === 'end') endMarker = node;
  694. }
  695. get('div[data-id="a"] input').type('aaa')
  696. get('div[data-id="b"] input').type('bbb')
  697. window.Alpine.morphBetween(startMarker, endMarker, `
  698. <div data-id="b">Item B Updated<input></div>
  699. <div data-id="c">Item C<input></div>
  700. <div data-id="a">Item A Updated<input></div>
  701. `, {
  702. key(el) { return el.dataset.id }
  703. })
  704. get('div[data-id]').should(haveLength(3))
  705. get('div[data-id="b"]').should(haveText('Item B Updated'))
  706. get('div[data-id="a"]').should(haveText('Item A Updated'))
  707. get('div[data-id="a"] input').should(haveValue('aaa'))
  708. get('div[data-id="b"] input').should(haveValue('bbb'))
  709. get('div[data-id="c"] input').should(haveValue(''))
  710. },
  711. )
  712. test('morphBetween with hooks',
  713. [html`
  714. <div>
  715. <!--region-start-->
  716. <p>Old paragraph</p>
  717. <span>Old span</span>
  718. <!--region-end-->
  719. </div>
  720. `],
  721. ({ get }, reload, window, document) => {
  722. // Find markers
  723. let startMarker, endMarker;
  724. const walker = document.createTreeWalker(
  725. document.body,
  726. NodeFilter.SHOW_COMMENT,
  727. null,
  728. false
  729. );
  730. let node;
  731. while (node = walker.nextNode()) {
  732. if (node.textContent === 'region-start') startMarker = node;
  733. if (node.textContent === 'region-end') endMarker = node;
  734. }
  735. let removedElements = []
  736. let addedElements = []
  737. window.Alpine.morphBetween(startMarker, endMarker, `
  738. <p>New paragraph</p>
  739. <article>New article</article>
  740. `, {
  741. removing(el) {
  742. if (el.nodeType === 1) removedElements.push(el.tagName)
  743. },
  744. adding(el) {
  745. if (el.nodeType === 1) addedElements.push(el.tagName)
  746. }
  747. })
  748. get('p').should(haveText('New paragraph'))
  749. get('article').should(haveText('New article'))
  750. // Check hooks were called
  751. cy.wrap(removedElements).should('deep.equal', ['SPAN'])
  752. cy.wrap(addedElements).should('deep.equal', ['ARTICLE'])
  753. },
  754. )
  755. test('morphBetween with empty content',
  756. [html`
  757. <div>
  758. <h3>Title</h3>
  759. <!--content-start-->
  760. <p>Content 1</p>
  761. <p>Content 2</p>
  762. <!--content-end-->
  763. <h3>End</h3>
  764. </div>
  765. `],
  766. ({ get }, reload, window, document) => {
  767. // Find markers
  768. let startMarker, endMarker;
  769. const walker = document.createTreeWalker(
  770. document.body,
  771. NodeFilter.SHOW_COMMENT,
  772. null,
  773. false
  774. );
  775. let node;
  776. while (node = walker.nextNode()) {
  777. if (node.textContent === 'content-start') startMarker = node;
  778. if (node.textContent === 'content-end') endMarker = node;
  779. }
  780. window.Alpine.morphBetween(startMarker, endMarker, '')
  781. get('h3').should(haveLength(2))
  782. get('p').should(haveLength(0))
  783. // Verify markers are still there
  784. let found = false;
  785. const walker2 = document.createTreeWalker(
  786. document.body,
  787. NodeFilter.SHOW_COMMENT,
  788. null,
  789. false
  790. );
  791. while (node = walker2.nextNode()) {
  792. if (node.textContent === 'content-start' || node.textContent === 'content-end') {
  793. found = true;
  794. }
  795. }
  796. cy.wrap(found).should('be.true')
  797. },
  798. )
  799. test('morphBetween with nested Alpine components',
  800. [html`
  801. <div x-data="{ outer: 'foo' }">
  802. <!--nested-start-->
  803. <div x-data="{ inner: 'bar' }">
  804. <span x-text="outer"></span>
  805. <span x-text="inner"></span>
  806. <input x-model="inner">
  807. </div>
  808. <!--nested-end-->
  809. </div>
  810. `],
  811. ({ get }, reload, window, document) => {
  812. // Find markers
  813. let startMarker, endMarker;
  814. const walker = document.createTreeWalker(
  815. document.body,
  816. NodeFilter.SHOW_COMMENT,
  817. null,
  818. false
  819. );
  820. let node;
  821. while (node = walker.nextNode()) {
  822. if (node.textContent === 'nested-start') startMarker = node;
  823. if (node.textContent === 'nested-end') endMarker = node;
  824. }
  825. get('span:nth-of-type(1)').should(haveText('foo'))
  826. get('span:nth-of-type(2)').should(haveText('bar'))
  827. get('input').clear().type('baz')
  828. get('span:nth-of-type(2)').should(haveText('baz'))
  829. window.Alpine.morphBetween(startMarker, endMarker, `
  830. <div x-data="{ inner: 'bar' }">
  831. <h4>New heading</h4>
  832. <span x-text="outer"></span>
  833. <span x-text="inner"></span>
  834. <input x-model="inner">
  835. </div>
  836. `)
  837. get('h4').should(haveText('New heading'))
  838. get('span:nth-of-type(1)').should(haveText('foo'))
  839. get('span:nth-of-type(2)').should(haveText('baz'))
  840. get('input').should(haveValue('baz'))
  841. },
  842. )
  843. test('morphBetween with conditional blocks',
  844. [html`
  845. <main>
  846. <!--section-start-->
  847. <!--[if BLOCK]><![endif]-->
  848. <div>conditional content<input></div>
  849. <!--[if ENDBLOCK]><![endif]-->
  850. <p>regular content<input></p>
  851. <!--section-end-->
  852. </main>
  853. `],
  854. ({ get }, reload, window, document) => {
  855. // Find markers
  856. let startMarker, endMarker;
  857. const walker = document.createTreeWalker(
  858. document.body,
  859. NodeFilter.SHOW_COMMENT,
  860. null,
  861. false
  862. );
  863. let node;
  864. while (node = walker.nextNode()) {
  865. if (node.textContent === 'section-start') startMarker = node;
  866. if (node.textContent === 'section-end') endMarker = node;
  867. }
  868. get('div input').type('div-value')
  869. get('p input').type('p-value')
  870. window.Alpine.morphBetween(startMarker, endMarker, `
  871. <!--[if BLOCK]><![endif]-->
  872. <div>conditional content<input></div>
  873. <span>new conditional<input></span>
  874. <!--[if ENDBLOCK]><![endif]-->
  875. <p>regular content<input></p>
  876. `)
  877. get('div input').should(haveValue('div-value'))
  878. get('span input').should(haveValue(''))
  879. get('p input').should(haveValue('p-value'))
  880. },
  881. )
  882. test('can ignore region between comment markers using skipUntil',
  883. [html`
  884. <ul>
  885. <li>foo</li>
  886. <!-- [Slot] -->
  887. <li>bar</li>
  888. <li>baz</li>
  889. <!-- [EndSlot] -->
  890. <!-- bob -->
  891. </ul>
  892. `],
  893. ({ get }, reload, window, document) => {
  894. // Generate "to" html without the items between Slot markers
  895. let toHtml = html`
  896. <ul>
  897. <li>foo</li>
  898. <!-- [Slot] -->
  899. <!-- [EndSlot] -->
  900. <!-- bob -->
  901. </ul>
  902. `
  903. // The original list should have 3 li's
  904. get('li').should(haveLength(3))
  905. get('li:nth-of-type(1)').should(haveText('foo'))
  906. get('li:nth-of-type(2)').should(haveText('bar'))
  907. get('li:nth-of-type(3)').should(haveText('baz'))
  908. // Run morph with custom updating hook that calls skipUntil
  909. let isStart = node => node && node.nodeType === 8 && node.textContent.trim() === '[Slot]'
  910. let isEnd = node => node && node.nodeType === 8 && node.textContent.trim() === '[EndSlot]'
  911. get('ul').then(([el]) => window.Alpine.morph(el, toHtml, {
  912. updating(from, to, childrenOnly, skip, skipChildren, skipUntil) {
  913. if (isStart(from) && isStart(to)) {
  914. skipUntil(node => isEnd(node))
  915. }
  916. },
  917. }))
  918. // After morph, the list should still contain the items inside the slot
  919. get('li').should(haveLength(3))
  920. get('li:nth-of-type(2)').should(haveText('bar'))
  921. get('li:nth-of-type(3)').should(haveText('baz'))
  922. },
  923. )