From 996d683144f73c4a764576977bd876c60f279064 Mon Sep 17 00:00:00 2001 From: Ricardo Devis Agullo Date: Tue, 14 Jul 2026 10:12:09 +0200 Subject: [PATCH] Avoid unmounting components during DOM moves Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- packages/oc-client-browser/src/oc-client.js | 36 ++++++----- .../tests/customElementsDomMove.spec.js | 64 +++++++++++++++++++ 2 files changed, 85 insertions(+), 15 deletions(-) create mode 100644 packages/oc-client-browser/tests/customElementsDomMove.spec.js diff --git a/packages/oc-client-browser/src/oc-client.js b/packages/oc-client-browser/src/oc-client.js index 1fc8f90ad..0124867fc 100644 --- a/packages/oc-client-browser/src/oc-client.js +++ b/packages/oc-client-browser/src/oc-client.js @@ -744,21 +744,27 @@ export function createOc(oc) { disconnectedCallback() { if (this.#connected) { this.#connected = false; - const id = this.getAttribute('id'); - if (id) { - oc.events.fire('oc:unrendered', { - element: this, - id - }); - } - const shouldUnmount = - this.#manageLifecycle && - this.unmount && - this.getAttribute('data-rendered') == 'true'; - if (shouldUnmount) { - this.unmount(); - this.removeAttribute('data-rendered'); - } + timeout(() => { + if (this.isConnected) { + return; + } + + const id = this.getAttribute('id'); + if (id) { + oc.events.fire('oc:unrendered', { + element: this, + id + }); + } + const shouldUnmount = + this.#manageLifecycle && + this.unmount && + this.getAttribute('data-rendered') == 'true'; + if (shouldUnmount) { + this.unmount(); + this.removeAttribute('data-rendered'); + } + }, 0); } } } diff --git a/packages/oc-client-browser/tests/customElementsDomMove.spec.js b/packages/oc-client-browser/tests/customElementsDomMove.spec.js new file mode 100644 index 000000000..3e5ee05ea --- /dev/null +++ b/packages/oc-client-browser/tests/customElementsDomMove.spec.js @@ -0,0 +1,64 @@ +import { expect, test } from '@playwright/test'; + +test.beforeEach(async ({ page }) => { + await page.goto('/'); +}); + +test.describe('oc-client : custom elements DOM moves', () => { + test('should keep rendered component mounted when moved in the DOM', async ({ + page + }) => { + const result = await page.evaluate(() => { + return new Promise((resolve) => { + let renderCalls = 0; + let unmountCalls = 0; + const originalRenderNested = oc.renderNestedComponent; + + oc.renderNestedComponent = (component, callback) => { + if (component.getAttribute('data-rendered') !== 'true') { + renderCalls++; + component.setAttribute('data-rendered', 'true'); + } + callback(); + }; + + const container = document.createElement('div'); + const component1 = document.createElement('oc-component'); + const component2 = document.createElement('oc-component'); + + component1.setAttribute('href', 'https://example.com/component-1'); + component2.setAttribute('href', 'https://example.com/component-2'); + component1.unmount = () => { + unmountCalls++; + }; + component2.unmount = () => { + unmountCalls++; + }; + + container.appendChild(component1); + container.appendChild(component2); + document.body.appendChild(container); + + setTimeout(() => { + container.insertBefore(component2, component1); + + setTimeout(() => { + oc.renderNestedComponent = originalRenderNested; + resolve({ + renderCalls, + unmountCalls, + firstHref: container.firstElementChild?.getAttribute('href'), + component2Rendered: + component2.getAttribute('data-rendered') === 'true' + }); + }, 50); + }, 50); + }); + }); + + expect(result.renderCalls).toBe(2); + expect(result.unmountCalls).toBe(0); + expect(result.firstHref).toBe('https://example.com/component-2'); + expect(result.component2Rendered).toBe(true); + }); +});