@@ -57,21 +57,122 @@ function updateNodeContent(oldNode: Node, newNode: Node): void {
5757 const oldChildren = Array . from ( oldEl . childNodes )
5858 const newChildren = Array . from ( newEl . childNodes )
5959
60- // Update existing children
61- const minLength = Math . min ( oldChildren . length , newChildren . length )
62- for ( let i = 0 ; i < minLength ; i ++ ) {
63- updateNodeContent ( oldChildren [ i ] , newChildren [ i ] )
64- }
60+ // Build a map of old children by ID for efficient lookup
61+ // Note: Multiple children can have the same ID (loop clones), so store arrays
62+ const oldChildrenById = new Map < string , Element [ ] > ( )
63+ const oldChildrenWithoutId : Node [ ] = [ ]
64+
65+ oldChildren . forEach ( child => {
66+ if ( child . nodeType === Node . ELEMENT_NODE ) {
67+ const id = ( child as Element ) . id
68+ if ( id ) {
69+ if ( ! oldChildrenById . has ( id ) ) {
70+ oldChildrenById . set ( id , [ ] )
71+ }
72+ oldChildrenById . get ( id ) ! . push ( child as Element )
73+ } else {
74+ oldChildrenWithoutId . push ( child )
75+ }
76+ } else {
77+ oldChildrenWithoutId . push ( child )
78+ }
79+ } )
6580
66- // Remove extra old children
67- for ( let i = oldChildren . length - 1 ; i >= minLength ; i -- ) {
68- oldEl . removeChild ( oldChildren [ i ] )
69- }
7081
71- // Add new children
72- for ( let i = minLength ; i < newChildren . length ; i ++ ) {
73- oldEl . appendChild ( newChildren [ i ] . cloneNode ( true ) )
74- }
82+ // Track which old children have been matched
83+ const matchedOldChildren = new Set < Node > ( )
84+
85+ // Process new children in order
86+ let lastInsertedNode : Node | null = null
87+
88+ newChildren . forEach ( newChild => {
89+ if ( newChild . nodeType === Node . ELEMENT_NODE ) {
90+ const newId = ( newChild as Element ) . id
91+
92+ // Try to find matching old child by ID
93+ if ( newId && oldChildrenById . has ( newId ) ) {
94+ const candidates = oldChildrenById . get ( newId ) !
95+ const oldChild = candidates . shift ( ) // Take first unmatched element
96+
97+ if ( ! oldChild ) {
98+ // All elements with this ID already matched, treat as no match
99+ const clonedNode = newChild . cloneNode ( true )
100+ if ( lastInsertedNode ) {
101+ oldEl . insertBefore ( clonedNode , lastInsertedNode . nextSibling )
102+ } else {
103+ oldEl . insertBefore ( clonedNode , oldEl . firstChild )
104+ }
105+ lastInsertedNode = clonedNode
106+ return
107+ }
108+
109+ matchedOldChildren . add ( oldChild )
110+
111+ // Update the existing element in place
112+ updateNodeContent ( oldChild , newChild )
113+
114+ // Ensure it's in the correct position
115+ if ( lastInsertedNode ) {
116+ if ( lastInsertedNode . nextSibling !== oldChild ) {
117+ oldEl . insertBefore ( oldChild , lastInsertedNode . nextSibling )
118+ }
119+ } else {
120+ if ( oldEl . firstChild !== oldChild ) {
121+ oldEl . insertBefore ( oldChild , oldEl . firstChild )
122+ }
123+ }
124+
125+ lastInsertedNode = oldChild
126+ } else {
127+ // No matching old child found, insert new element
128+ const clonedNode = newChild . cloneNode ( true )
129+ if ( lastInsertedNode ) {
130+ oldEl . insertBefore ( clonedNode , lastInsertedNode . nextSibling )
131+ } else {
132+ oldEl . insertBefore ( clonedNode , oldEl . firstChild )
133+ }
134+ lastInsertedNode = clonedNode
135+ }
136+ } else {
137+ // Text node or other - try to match with old children without IDs
138+ const matchIndex = oldChildrenWithoutId . findIndex ( oldChild =>
139+ ! matchedOldChildren . has ( oldChild ) && oldChild . nodeType === newChild . nodeType
140+ )
141+
142+ if ( matchIndex >= 0 ) {
143+ const oldChild = oldChildrenWithoutId [ matchIndex ]
144+ matchedOldChildren . add ( oldChild )
145+ updateNodeContent ( oldChild , newChild )
146+
147+ if ( lastInsertedNode ) {
148+ if ( lastInsertedNode . nextSibling !== oldChild ) {
149+ oldEl . insertBefore ( oldChild , lastInsertedNode . nextSibling )
150+ }
151+ } else {
152+ if ( oldEl . firstChild !== oldChild ) {
153+ oldEl . insertBefore ( oldChild , oldEl . firstChild )
154+ }
155+ }
156+
157+ lastInsertedNode = oldChild
158+ } else {
159+ const clonedNode = newChild . cloneNode ( true )
160+ if ( lastInsertedNode ) {
161+ oldEl . insertBefore ( clonedNode , lastInsertedNode . nextSibling )
162+ } else {
163+ oldEl . insertBefore ( clonedNode , oldEl . firstChild )
164+ }
165+ lastInsertedNode = clonedNode
166+ }
167+ }
168+ } )
169+
170+ // Remove old children that weren't matched
171+ oldChildren . forEach ( oldChild => {
172+ if ( ! matchedOldChildren . has ( oldChild ) ) {
173+ oldEl . removeChild ( oldChild )
174+ }
175+ } )
75176 }
76177}
77178
@@ -308,7 +409,6 @@ function renderContent(comp: Component, deep: number) {
308409 const innerHtml = renderInnerHTML ( comp )
309410
310411 if ( innerHtml === null ) {
311- comp . view ! . render ( )
312412 comp . components ( )
313413 . forEach ( c => renderPreview ( c , deep + 1 ) )
314414 } else {
@@ -367,6 +467,15 @@ export function renderPreview(comp: Component, deep = 0) {
367467 if ( __data . length === 0 ) {
368468 el . remove ( )
369469 } else {
470+ // Remove all existing loop clones (siblings with same ID from previous render)
471+ const componentId = el . id
472+ let nextSibling = el . nextElementSibling as HTMLElement | null
473+ while ( nextSibling && nextSibling . id === componentId ) {
474+ const toRemove = nextSibling
475+ nextSibling = nextSibling . nextElementSibling as HTMLElement | null
476+ toRemove . remove ( )
477+ }
478+
370479 const initialPreviewIndex = getPreviewIndex ( comp ) || 0
371480
372481 // Render each loop iteration
@@ -387,11 +496,37 @@ export function renderPreview(comp: Component, deep = 0) {
387496
388497 // For subsequent iterations: clone first, then render into original (without diffing), then repeat
389498 for ( let idx = fromIdx - 1 ; idx >= toIdx ; idx -- ) {
499+ // Check if this iteration should be visible
500+ setPreviewIndexToLoopData ( comp , idx )
501+ const isVisibleAtIdx = isComponentVisible ( comp )
502+
503+ // Skip invisible iterations - don't create a clone
504+ if ( ! isVisibleAtIdx ) {
505+ continue
506+ }
507+
390508 // Clone the current state (with previous iteration's content)
391509 const clone = el . cloneNode ( true ) as HTMLElement
392510
393511 clone . classList . remove ( 'gjs-selected' )
394512
513+ // Remove hidden elements from the clone to prevent them from appearing in the output
514+ const hiddenInClone = clone . querySelectorAll ( '[style*="display: none"]' )
515+ hiddenInClone . forEach ( hiddenEl => hiddenEl . remove ( ) )
516+
517+ // Reset visibility on the ORIGINAL element's children before rendering next iteration
518+ // This ensures the next iteration starts with all elements visible
519+ const hiddenInOriginal = el . querySelectorAll ( '[style*="display: none"]' )
520+ hiddenInOriginal . forEach ( hiddenEl => {
521+ if ( hiddenEl instanceof HTMLElement && hiddenEl . style ) {
522+ hiddenEl . style . removeProperty ( 'display' )
523+ // Remove empty style attribute
524+ if ( hiddenEl . style . length === 0 ) {
525+ hiddenEl . removeAttribute ( 'style' )
526+ }
527+ }
528+ } )
529+
395530 // Keep the selection mechanism - use GrapesJS component API
396531 clone . addEventListener ( 'click' , ( event ) => {
397532 const clickedElement = event . target as HTMLElement
@@ -469,37 +604,42 @@ export function renderPreview(comp: Component, deep = 0) {
469604 // Add the clone to the canvas
470605 el . insertAdjacentElement ( 'afterend' , clone )
471606
472- // Set preview index for the next iteration and render into original element
607+ // Render the current iteration into the original element
473608 // NOTE: We skip the diffing here because we're in a loop creating initial elements
474609 // Diffing only helps on re-renders, not initial renders
475- setPreviewIndexToLoopData ( comp , idx )
476- const isVisibleAtIdx = isComponentVisible ( comp )
477-
478- if ( isVisibleAtIdx ) {
479- const innerHtml = renderInnerHTML ( comp )
480- if ( innerHtml === null ) {
481- comp . view ! . render ( )
482- comp . components ( )
483- . forEach ( c => renderPreview ( c , deep + 1 ) )
484- } else {
485- // Just set innerHTML directly without diffing in the loop
486- el . innerHTML = innerHtml
487- }
488- renderAttributes ( comp )
610+ const innerHtml = renderInnerHTML ( comp )
611+ if ( innerHtml === null ) {
612+ comp . components ( )
613+ . forEach ( c => renderPreview ( c , deep + 1 ) )
489614 } else {
490- el . remove ( )
615+ // Just set innerHTML directly without diffing in the loop
616+ el . innerHTML = innerHtml
491617 }
618+ renderAttributes ( comp )
492619 }
493620 setPreviewIndexToLoopData ( comp , initialPreviewIndex )
494621 }
495622 } else {
496623 const isVisible = isComponentVisible ( comp )
497624
498625 if ( isVisible ) {
626+ // Make sure the element is visible (in case it was hidden before)
627+ if ( el . style && el . style . display === 'none' ) {
628+ el . style . removeProperty ( 'display' )
629+ // Remove empty style attribute
630+ if ( el . style . length === 0 ) {
631+ el . removeAttribute ( 'style' )
632+ }
633+ }
499634 renderContent ( comp , deep )
500635 renderAttributes ( comp )
501636 } else {
502- el . remove ( )
637+ // Don't remove the element, just hide it
638+ // This prevents breaking loop rendering where the same element
639+ // is referenced across multiple iterations
640+ if ( el . parentElement && el . style ) {
641+ el . style . display = 'none'
642+ }
503643 }
504644 }
505645}
0 commit comments