diff --git a/src/runtime/vdom/vdom-render.ts b/src/runtime/vdom/vdom-render.ts index 1521bb811ae..99a0ec24e57 100644 --- a/src/runtime/vdom/vdom-render.ts +++ b/src/runtime/vdom/vdom-render.ts @@ -154,9 +154,15 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: // check if we've got an old vnode for this slot oldVNode = oldParentVNode && oldParentVNode.$children$ && oldParentVNode.$children$[childIndex]; if (oldVNode && oldVNode.$tag$ === newVNode.$tag$ && oldParentVNode.$elm$) { - // we've got an old slot vnode and the wrapper is being replaced - // so let's move the old slot content back to it's original location - putBackInOriginalLocation(oldParentVNode.$elm$, false); + if (BUILD.experimentalSlotFixes) { + // we've got an old slot vnode and the wrapper is being replaced + // so let's move the old slot content to the root of the element currently being rendered + relocateToHostRoot(oldParentVNode.$elm$); + } else { + // we've got an old slot vnode and the wrapper is being replaced + // so let's move the old slot content back to its original location + putBackInOriginalLocation(oldParentVNode.$elm$, false); + } } } } @@ -164,6 +170,43 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: return elm; }; +/** + * Relocates all child nodes of an element that were a part of a previous slot relocation + * to the root of the Stencil component currently being rendered. This happens when a parent + * element of a slot reference node dynamically changes and triggers a re-render. We cannot use + * `putBackInOriginalLocation()` because that may relocate nodes to elements that will not be re-rendered + * and so they will not be relocated again. + * + * @param parentElm The element potentially containing relocated nodes. + */ +const relocateToHostRoot = (parentElm: Element) => { + plt.$flags$ |= PLATFORM_FLAGS.isTmpDisconnected; + + const host = parentElm.closest(hostTagName.toLowerCase()); + if (host != null) { + for (const childNode of Array.from(parentElm.childNodes) as d.RenderNode[]) { + // Only relocate nodes that were slotted in + if (childNode['s-sh'] != null) { + host.insertBefore(childNode, null); + // Reset so we can correctly move the node around again. + childNode['s-sh'] = undefined; + + // When putting an element node back in its original location, + // we need to reset the `slot` attribute back to the value it originally had + // so we can correctly relocate it again in the future + if (childNode.nodeType === NODE_TYPE.ElementNode && !!childNode['s-sn']) { + childNode.setAttribute('slot', childNode['s-sn']); + } + + // Need to tell the render pipeline to check to relocate slot content again + checkSlotRelocate = true; + } + } + } + + plt.$flags$ &= ~PLATFORM_FLAGS.isTmpDisconnected; +}; + const putBackInOriginalLocation = (parentElm: Node, recursive: boolean) => { plt.$flags$ |= PLATFORM_FLAGS.isTmpDisconnected; @@ -171,10 +214,6 @@ const putBackInOriginalLocation = (parentElm: Node, recursive: boolean) => { for (let i = oldSlotChildNodes.length - 1; i >= 0; i--) { const childNode = oldSlotChildNodes[i] as any; if (childNode['s-hn'] !== hostTagName && childNode['s-ol']) { - // // this child node in the old element is from another component - // // remove this node from the old slot's parent - // childNode.remove(); - // and relocate it back to it's original location parentReferenceNode(childNode).insertBefore(childNode, referenceNode(childNode)); @@ -1027,7 +1066,7 @@ render() { // has a different next sibling or parent relocated if (nodeToRelocate !== insertBeforeNode) { - if (!nodeToRelocate['s-hn'] && nodeToRelocate['s-ol']) { + if (!BUILD.experimentalSlotFixes && !nodeToRelocate['s-hn'] && nodeToRelocate['s-ol']) { // probably a component in the index.html that doesn't have its hostname set nodeToRelocate['s-hn'] = nodeToRelocate['s-ol'].parentNode.nodeName; } diff --git a/test/karma/test-app/components.d.ts b/test/karma/test-app/components.d.ts index 6061c71ae71..9bec03c8e15 100644 --- a/test/karma/test-app/components.d.ts +++ b/test/karma/test-app/components.d.ts @@ -351,6 +351,12 @@ export namespace Components { } interface SlotNoDefault { } + interface SlotParentTagChange { + "element": string; + } + interface SlotParentTagChangeRoot { + "element": string; + } interface SlotReorder { "reordered": boolean; } @@ -1306,6 +1312,18 @@ declare global { prototype: HTMLSlotNoDefaultElement; new (): HTMLSlotNoDefaultElement; }; + interface HTMLSlotParentTagChangeElement extends Components.SlotParentTagChange, HTMLStencilElement { + } + var HTMLSlotParentTagChangeElement: { + prototype: HTMLSlotParentTagChangeElement; + new (): HTMLSlotParentTagChangeElement; + }; + interface HTMLSlotParentTagChangeRootElement extends Components.SlotParentTagChangeRoot, HTMLStencilElement { + } + var HTMLSlotParentTagChangeRootElement: { + prototype: HTMLSlotParentTagChangeRootElement; + new (): HTMLSlotParentTagChangeRootElement; + }; interface HTMLSlotReorderElement extends Components.SlotReorder, HTMLStencilElement { } var HTMLSlotReorderElement: { @@ -1538,6 +1556,8 @@ declare global { "slot-nested-order-parent": HTMLSlotNestedOrderParentElement; "slot-ng-if": HTMLSlotNgIfElement; "slot-no-default": HTMLSlotNoDefaultElement; + "slot-parent-tag-change": HTMLSlotParentTagChangeElement; + "slot-parent-tag-change-root": HTMLSlotParentTagChangeRootElement; "slot-reorder": HTMLSlotReorderElement; "slot-reorder-root": HTMLSlotReorderRootElement; "slot-replace-wrapper": HTMLSlotReplaceWrapperElement; @@ -1904,6 +1924,12 @@ declare namespace LocalJSX { } interface SlotNoDefault { } + interface SlotParentTagChange { + "element"?: string; + } + interface SlotParentTagChangeRoot { + "element"?: string; + } interface SlotReorder { "reordered"?: boolean; } @@ -2074,6 +2100,8 @@ declare namespace LocalJSX { "slot-nested-order-parent": SlotNestedOrderParent; "slot-ng-if": SlotNgIf; "slot-no-default": SlotNoDefault; + "slot-parent-tag-change": SlotParentTagChange; + "slot-parent-tag-change-root": SlotParentTagChangeRoot; "slot-reorder": SlotReorder; "slot-reorder-root": SlotReorderRoot; "slot-replace-wrapper": SlotReplaceWrapper; @@ -2231,6 +2259,8 @@ declare module "@stencil/core" { "slot-nested-order-parent": LocalJSX.SlotNestedOrderParent & JSXBase.HTMLAttributes; "slot-ng-if": LocalJSX.SlotNgIf & JSXBase.HTMLAttributes; "slot-no-default": LocalJSX.SlotNoDefault & JSXBase.HTMLAttributes; + "slot-parent-tag-change": LocalJSX.SlotParentTagChange & JSXBase.HTMLAttributes; + "slot-parent-tag-change-root": LocalJSX.SlotParentTagChangeRoot & JSXBase.HTMLAttributes; "slot-reorder": LocalJSX.SlotReorder & JSXBase.HTMLAttributes; "slot-reorder-root": LocalJSX.SlotReorderRoot & JSXBase.HTMLAttributes; "slot-replace-wrapper": LocalJSX.SlotReplaceWrapper & JSXBase.HTMLAttributes; diff --git a/test/karma/test-app/slot-parent-tag-change/cmp-root.tsx b/test/karma/test-app/slot-parent-tag-change/cmp-root.tsx new file mode 100644 index 00000000000..ea1b808cb84 --- /dev/null +++ b/test/karma/test-app/slot-parent-tag-change/cmp-root.tsx @@ -0,0 +1,16 @@ +import { Component, h, Prop } from '@stencil/core'; + +@Component({ + tag: 'slot-parent-tag-change-root', +}) +export class SlotParentTagChangeRoot { + @Prop() element = 'p'; + + render() { + return ( + + + + ); + } +} diff --git a/test/karma/test-app/slot-parent-tag-change/cmp.tsx b/test/karma/test-app/slot-parent-tag-change/cmp.tsx new file mode 100644 index 00000000000..84d9bda129e --- /dev/null +++ b/test/karma/test-app/slot-parent-tag-change/cmp.tsx @@ -0,0 +1,16 @@ +import { Component, h, Prop } from '@stencil/core'; + +@Component({ + tag: 'slot-parent-tag-change', +}) +export class SlotParentTagChange { + @Prop() element = 'p'; + + render() { + return ( + + + + ); + } +} diff --git a/test/karma/test-app/slot-parent-tag-change/index.html b/test/karma/test-app/slot-parent-tag-change/index.html new file mode 100644 index 00000000000..56ac733787b --- /dev/null +++ b/test/karma/test-app/slot-parent-tag-change/index.html @@ -0,0 +1,21 @@ + + + + + + + Hello + + World + + + + + diff --git a/test/karma/test-app/slot-parent-tag-change/karma.spec.ts b/test/karma/test-app/slot-parent-tag-change/karma.spec.ts new file mode 100644 index 00000000000..621e2bdd3dd --- /dev/null +++ b/test/karma/test-app/slot-parent-tag-change/karma.spec.ts @@ -0,0 +1,52 @@ +import { setupDomTests, waitForChanges } from '../util'; + +/** + * Tests the cases where a node is slotted in from the root `index.html` file + * and the slot's parent element dynamically changes (e.g. from `p` to `span`). + */ +describe('slot-parent-tag-change', () => { + const { setupDom, tearDownDom } = setupDomTests(document); + let app: HTMLElement; + + beforeEach(async () => { + app = await setupDom('/slot-parent-tag-change/index.html'); + }); + + afterEach(tearDownDom); + + describe('direct slot', () => { + it('should relocate the text node to the slot after the parent tag changes', async () => { + const root = app.querySelector('#top-level'); + + expect(root).not.toBeNull(); + expect(root.children.length).toBe(1); + expect(root.children[0].tagName).toBe('P'); + expect(root.children[0].textContent.trim()).toBe('Hello'); + + app.querySelector('#top-level-button').click(); + await waitForChanges(); + + expect(root.children.length).toBe(1); + expect(root.children[0].tagName).toBe('SPAN'); + expect(root.children[0].textContent.trim()).toBe('Hello'); + }); + }); + + describe('nested slot', () => { + it('should relocate the text node to the slot after the parent tag changes', async () => { + const root = app.querySelector('slot-parent-tag-change-root slot-parent-tag-change'); + + expect(root).not.toBeNull(); + expect(root.children.length).toBe(1); + expect(root.children[0].tagName).toBe('P'); + expect(root.children[0].textContent.trim()).toBe('World'); + + app.querySelector('#nested-button').click(); + await waitForChanges(); + + expect(root.children.length).toBe(1); + expect(root.children[0].tagName).toBe('SPAN'); + expect(root.children[0].textContent.trim()).toBe('World'); + }); + }); +});