@@ -27,14 +27,48 @@ export function executeMorphdom(
27
27
* To handle them, we:
28
28
* 1) Create an array of the "current" HTMLElements that match each
29
29
* "data-live-preserve" element.
30
- * 2) Replace the "current" elements with clones so that the originals
31
- * aren't modified during the morphing process.
32
- * 3) After the morphing is complete, we find the preserved elements and
33
- * replace them with the originals.
30
+ * 2) If we detect that a "current" preserved element is about to be morphed
31
+ * (updated or removed), replace the "current" element with a clone so
32
+ * the original isn't modified during the morphing process. Mark that
33
+ * this element needs to be replaced after the morphing is complete.
34
+ * 3) After the morphing is complete, loop over the elements that were
35
+ * replaced and swap the original element back into the new position.
36
+ *
37
+ * This allows Idiomorph to potentially morph the position of the preserved
38
+ * elements... but still allowing us to make sure that the element in the
39
+ * new position is exactly the original HTMLElement.
40
+ */
41
+ const originalElementIdsToSwapAfter : Array < string > = [ ] ;
42
+ const originalElementsToPreserve = new Map < string , HTMLElement > ( ) ;
43
+
44
+ /**
45
+ * Called when a preserved element is about to be morphed.
46
+ *
47
+ * Instead of allowing the original to be morphed, a fake clone
48
+ * is created and morphed instead. The original is then marked
49
+ * to be replaced after the morph with wherever the final
50
+ * matching id element ends up.
34
51
*/
35
- const preservedOriginalElements : HTMLElement [ ] = [ ] ;
52
+ const markElementAsNeedingPostMorphSwap = ( id : string , replaceWithClone : boolean ) : HTMLElement | null => {
53
+ const oldElement = originalElementsToPreserve . get ( id ) ;
54
+ if ( ! ( oldElement instanceof HTMLElement ) ) {
55
+ throw new Error ( `Original element with id ${ id } not found` ) ;
56
+ }
57
+
58
+ originalElementIdsToSwapAfter . push ( id ) ;
59
+ if ( ! replaceWithClone ) {
60
+ return null ;
61
+ }
62
+
63
+ const clonedOldElement = cloneHTMLElement ( oldElement ) ;
64
+ oldElement . replaceWith ( clonedOldElement ) ;
65
+
66
+ return clonedOldElement ;
67
+ } ;
68
+
36
69
rootToElement . querySelectorAll ( '[data-live-preserve]' ) . forEach ( ( newElement ) => {
37
70
const id = newElement . id ;
71
+
38
72
if ( ! id ) {
39
73
throw new Error ( 'The data-live-preserve attribute requires an id attribute to be set on the element' ) ;
40
74
}
@@ -44,11 +78,8 @@ export function executeMorphdom(
44
78
throw new Error ( `The element with id "${ id } " was not found in the original HTML` ) ;
45
79
}
46
80
47
- const clonedOldElement = cloneHTMLElement ( oldElement ) ;
48
- preservedOriginalElements . push ( oldElement ) ;
49
- oldElement . replaceWith ( clonedOldElement ) ;
50
-
51
81
newElement . removeAttribute ( 'data-live-preserve' ) ;
82
+ originalElementsToPreserve . set ( id , oldElement ) ;
52
83
syncAttributes ( newElement , oldElement ) ;
53
84
} ) ;
54
85
@@ -64,6 +95,27 @@ export function executeMorphdom(
64
95
return true ;
65
96
}
66
97
98
+ if ( fromEl . id && originalElementsToPreserve . has ( fromEl . id ) ) {
99
+ if ( fromEl . id === toEl . id ) {
100
+ // the preserved elements match, prevent morph and
101
+ // keep the original element
102
+ return false ;
103
+ }
104
+
105
+ // a preserved element is being morphed into something else
106
+ // this means that preserved element is being moved
107
+ // to avoid the original element being morphed, we swap
108
+ // it for a clone, manually morph the clone, and then
109
+ // skip trying to morph the original element (we want it untouched)
110
+ const clonedFromEl = markElementAsNeedingPostMorphSwap ( fromEl . id , true ) ;
111
+ if ( ! clonedFromEl ) {
112
+ throw new Error ( 'missing clone' ) ;
113
+ }
114
+ Idiomorph . morph ( clonedFromEl , toEl ) ;
115
+
116
+ return false ;
117
+ }
118
+
67
119
// skip special checking if this is, for example, an SVG
68
120
if ( fromEl instanceof HTMLElement && toEl instanceof HTMLElement ) {
69
121
// We assume fromEl is an Alpine component if it has `__x` property.
@@ -168,6 +220,18 @@ export function executeMorphdom(
168
220
return true ;
169
221
}
170
222
223
+ if ( node . id && originalElementsToPreserve . has ( node . id ) ) {
224
+ // a preserved element is being removed
225
+ // to avoid the original element being destroyed (but still
226
+ // allowing this spot on the dom to be removed),
227
+ // clone the original element and place it into the
228
+ // new position after morphing
229
+ markElementAsNeedingPostMorphSwap ( node . id , false ) ;
230
+
231
+ // allow this to be morphed to the new element
232
+ return true ;
233
+ }
234
+
171
235
if ( externalMutationTracker . wasElementAdded ( node ) ) {
172
236
// this element was added by an external mutation, so we don't want to discard it
173
237
return false ;
@@ -178,14 +242,13 @@ export function executeMorphdom(
178
242
} ,
179
243
} ) ;
180
244
181
- preservedOriginalElements . forEach ( ( oldElement ) => {
182
- const newElement = rootFromElement . querySelector ( `#${ oldElement . id } ` ) ;
183
- if ( ! ( newElement instanceof HTMLElement ) ) {
184
- // should not happen, as preservedOriginalElements is built from
185
- // the new HTML
186
- throw new Error ( 'Missing preserved element ' ) ;
245
+ originalElementIdsToSwapAfter . forEach ( ( id : string ) => {
246
+ const newElement = rootFromElement . querySelector ( `#${ id } ` ) ;
247
+ const originalElement = originalElementsToPreserve . get ( id ) ;
248
+ if ( ! ( newElement instanceof HTMLElement ) || ! ( originalElement instanceof HTMLElement ) ) {
249
+ // should not happen
250
+ throw new Error ( 'Missing elements. ' ) ;
187
251
}
188
-
189
- newElement . replaceWith ( oldElement ) ;
252
+ newElement . replaceWith ( originalElement ) ;
190
253
} ) ;
191
254
}
0 commit comments