-
-
Notifications
You must be signed in to change notification settings - Fork 413
Bug: data-live-preserve breaks when parent element's ID changes during morph #3423
Description
Symfony version(s) affected
2.34.0
Description
Elements with data-live-preserve lose their preserved DOM state when any ancestor element's ID changes between re-renders. The innerHTML shortcut path in the morphdom logic bypasses the preserve mechanism entirely, causing the preserved element to be replaced with a freshly parsed DOM node.
How to reproduce
<div id="parent-live-component">
<div id="container-with-random-id-on-every-render-of-parent-component-123">
<div id="child-live-component">...</div>
</div>
</div>After rerender of "parent-live-component", server returns something like this
<div id="parent-live-component">
<div id="container-with-random-id-on-every-render-of-parent-component-789">
<div id="child-live-component" data-live-preserve></div>
</div>
</div>The child component, which should be preserved loses all its DOM state.
Root cause
In https://github.com/symfony/ux/blob/2.x/src/LiveComponent/assets/src/morphdom.ts, the beforeNodeMorphed callback has this logic:
if (fromEl.hasAttribute('data-skip-morph') || (fromEl.id && fromEl.id !== toEl.id)) {
fromEl.innerHTML = toEl.innerHTML;
return true;
}When a parent element's ID changes (fromEl.id !== toEl.id), this code does a brute-force innerHTML replacement of all children. This is a native DOM operation — Idiomorph's callbacks (beforeNodeMorphed, beforeNodeRemoved) are never invoked for any of the children.
The data-live-preserve mechanism relies on those callbacks to skip morphing preserved elements. Since they're never called, the preserved element is silently destroyed and replaced by a fresh element parsed from the HTML string. The original element remains orphaned in the originalElementsToPreserve map, and its ID is never added to originalElementIdsToSwapAfter, so the post-morph swap never runs.
Suggested fix
After the innerHTML swap, restore any preserved elements that were inside the affected parent:
if (fromEl.hasAttribute('data-skip-morph') || (fromEl.id && fromEl.id !== toEl.id)) {
fromEl.innerHTML = toEl.innerHTML;
// Restore preserved elements destroyed by innerHTML swap
originalElementsToPreserve.forEach((originalElement, id) => {
const placeholder = fromEl.querySelector(`#${id}`);
if (placeholder) {
syncAttributes(placeholder, originalElement);
placeholder.replaceWith(originalElement);
}
});
return true;
}Possible Solution
No response
Additional Context
No response