diff --git a/src/declarations/stencil-private.ts b/src/declarations/stencil-private.ts index 20d6014f8dd..6ac1c6b508b 100644 --- a/src/declarations/stencil-private.ts +++ b/src/declarations/stencil-private.ts @@ -1331,6 +1331,12 @@ export interface RenderNode extends HostElement { */ host?: Element; + /** + * On Ref Function: + * Callback function to be called when the slotted node ref is ready. + */ + ['s-rf']?: (elm: Element) => unknown; + /** * Is initially hidden * Whether this node was originally rendered with the `hidden` attribute. diff --git a/src/runtime/dom-extras.ts b/src/runtime/dom-extras.ts index 588b8fc9199..ed1a369737c 100644 --- a/src/runtime/dom-extras.ts +++ b/src/runtime/dom-extras.ts @@ -47,6 +47,7 @@ export const patchCloneNode = (HostElementPrototype: HTMLElement) => { 's-ol', 's-nr', 's-si', + 's-rf', ]; for (; i < srcNode.childNodes.length; i++) { diff --git a/src/runtime/vdom/vdom-render.ts b/src/runtime/vdom/vdom-render.ts index e465e4b37dd..6de2d461022 100644 --- a/src/runtime/vdom/vdom-render.ts +++ b/src/runtime/vdom/vdom-render.ts @@ -149,6 +149,9 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: // remember the slot name, or empty string for default slot elm['s-sn'] = newVNode.$name$ || ''; + // remember the ref callback function + elm['s-rf'] = newVNode.$attrs$?.ref; + // 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$) { @@ -1094,6 +1097,8 @@ render() { } } } + + nodeToRelocate && typeof slotRefNode['s-rf'] === 'function' && slotRefNode['s-rf'](nodeToRelocate); } else { // this node doesn't have a slot home to go to, so let's hide it if (nodeToRelocate.nodeType === NODE_TYPE.ElementNode) { diff --git a/test/karma/test-app/components.d.ts b/test/karma/test-app/components.d.ts index 11a67cb356c..30b36230f0d 100644 --- a/test/karma/test-app/components.d.ts +++ b/test/karma/test-app/components.d.ts @@ -369,6 +369,8 @@ export namespace Components { interface SlotParentTagChangeRoot { "element": string; } + interface SlotRef { + } interface SlotReorder { "reordered": boolean; } @@ -1373,6 +1375,12 @@ declare global { prototype: HTMLSlotParentTagChangeRootElement; new (): HTMLSlotParentTagChangeRootElement; }; + interface HTMLSlotRefElement extends Components.SlotRef, HTMLStencilElement { + } + var HTMLSlotRefElement: { + prototype: HTMLSlotRefElement; + new (): HTMLSlotRefElement; + }; interface HTMLSlotReorderElement extends Components.SlotReorder, HTMLStencilElement { } var HTMLSlotReorderElement: { @@ -1622,6 +1630,7 @@ declare global { "slot-no-default": HTMLSlotNoDefaultElement; "slot-parent-tag-change": HTMLSlotParentTagChangeElement; "slot-parent-tag-change-root": HTMLSlotParentTagChangeRootElement; + "slot-ref": HTMLSlotRefElement; "slot-reorder": HTMLSlotReorderElement; "slot-reorder-root": HTMLSlotReorderRootElement; "slot-replace-wrapper": HTMLSlotReplaceWrapperElement; @@ -2008,6 +2017,8 @@ declare namespace LocalJSX { interface SlotParentTagChangeRoot { "element"?: string; } + interface SlotRef { + } interface SlotReorder { "reordered"?: boolean; } @@ -2187,6 +2198,7 @@ declare namespace LocalJSX { "slot-no-default": SlotNoDefault; "slot-parent-tag-change": SlotParentTagChange; "slot-parent-tag-change-root": SlotParentTagChangeRoot; + "slot-ref": SlotRef; "slot-reorder": SlotReorder; "slot-reorder-root": SlotReorderRoot; "slot-replace-wrapper": SlotReplaceWrapper; @@ -2351,6 +2363,7 @@ declare module "@stencil/core" { "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-ref": LocalJSX.SlotRef & 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-ref/cmp.tsx b/test/karma/test-app/slot-ref/cmp.tsx new file mode 100644 index 00000000000..6dfbdddb564 --- /dev/null +++ b/test/karma/test-app/slot-ref/cmp.tsx @@ -0,0 +1,24 @@ +import { Component, Element, h, Host } from '@stencil/core'; + +@Component({ + tag: 'slot-ref', + shadow: false, + scoped: true, +}) +export class SlotRef { + @Element() hostElement: HTMLElement; + + render() { + return ( + + { + this.hostElement.setAttribute('data-ref-id', el.id); + this.hostElement.setAttribute('data-ref-tagname', el.tagName); + }} + /> + + ); + } +} diff --git a/test/karma/test-app/slot-ref/index.html b/test/karma/test-app/slot-ref/index.html new file mode 100644 index 00000000000..8c4df4ee1e0 --- /dev/null +++ b/test/karma/test-app/slot-ref/index.html @@ -0,0 +1,6 @@ + + + + + +Hello World! diff --git a/test/karma/test-app/slot-ref/karma.spec.ts b/test/karma/test-app/slot-ref/karma.spec.ts new file mode 100644 index 00000000000..421422aa0a7 --- /dev/null +++ b/test/karma/test-app/slot-ref/karma.spec.ts @@ -0,0 +1,19 @@ +import { setupDomTests, waitForChanges } from '../util'; + +describe('slot-ref', function () { + const { setupDom, tearDownDom } = setupDomTests(document); + let app: HTMLElement; + + beforeEach(async () => { + app = await setupDom('/slot-ref/index.html'); + }); + afterEach(tearDownDom); + + it('ref callback of slot is called', async () => { + await waitForChanges(); + + const host = app.querySelector('slot-ref'); + expect(host.getAttribute('data-ref-id')).toBe('slotted-element-id'); + expect(host.getAttribute('data-ref-tagname')).toBe('SPAN'); + }); +});