Skip to content

Commit

Permalink
feat(no-node slot fallback content): removed all <slot-fb> nodes for …
Browse files Browse the repository at this point in the history
…fallback content.

feat(patch remove / removeChild): non-shadow components will show/hide fallback content on slot node addition/removal
  • Loading branch information
johnjenkins committed Jun 30, 2021
1 parent 4e8b9bd commit f9a0063
Show file tree
Hide file tree
Showing 13 changed files with 314 additions and 206 deletions.
15 changes: 15 additions & 0 deletions src/declarations/stencil-private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1530,6 +1530,21 @@ export interface RenderNode extends HostElement {
*/
['s-sn']?: string;

/**
* Is a slot fallback node
*/
['s-sf']?: boolean;

/**
* Slot has fallback nodes
*/
['s-hsf']?: boolean;

/**
* Slot fallback node text content
*/
['s-sfc']?: string;

/**
* Host element tag name:
* The tag name of the host element that this
Expand Down
14 changes: 13 additions & 1 deletion src/declarations/stencil-public-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ export declare const setMode: (handler: ResolutionHandler) => void;
*/
export declare function getMode<T = string | undefined>(ref: any): T;

export declare function setPlatformHelpers (helpers: {
export declare function setPlatformHelpers (helpers: {
jmp?: (c: any) => any;
raf?: (c: any) => number;
ael?: (
Expand Down Expand Up @@ -320,6 +320,18 @@ export declare function getRenderingRef(): any;

export interface HTMLStencilElement extends HTMLElement {
componentOnReady(): Promise<this>;
readonly __childNodes?: NodeListOf<Node>;
readonly __children?: HTMLCollectionOf<Element>;
readonly __childElementCount?: number;
__innerHTML?: string;
__innerText?: string;
__append?: (...nodes: (Node | string)[]) => void
__prepend?: (...nodes: (Node | string)[]) => void;
__appendChild?: <T extends Node>(newChild: T) => T;
__replaceChildren?: (...nodes: (Node | string)[]) => void
__insertAdjacentElement?: (position: InsertPosition, insertedElement: Element) => Element | null;
__insertAdjacentHTML?: (where: InsertPosition, html: string) => void;
__insertAdjacentText?: (where: InsertPosition, text: string) => void;
}

/**
Expand Down
5 changes: 4 additions & 1 deletion src/runtime/bootstrap-custom-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { disconnectedCallback } from './disconnected-callback';
import { forceUpdate, getHostRef, registerHost, styles, supportsShadow } from '@platform';
import { proxyComponent } from './proxy-component';
import { PROXY_FLAGS } from './runtime-constants';
import { patchPseudoShadowDom } from './dom-extras';
import { patchCloneNode, patchPseudoShadowDom } from './dom-extras';

export const defineCustomElement = (Cstr: any, compactMeta: d.ComponentRuntimeMetaCompact) => {
customElements.define(compactMeta[1], proxyCustomElement(Cstr, compactMeta) as CustomElementConstructor);
Expand Down Expand Up @@ -40,6 +40,9 @@ export const proxyCustomElement = (Cstr: any, compactMeta: d.ComponentRuntimeMet
(cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation && CMP_FLAGS.needsShadowDomShim)
) {
patchPseudoShadowDom(Cstr.prototype);
if (BUILD.cloneNodeFix) {
patchCloneNode(Cstr.prototype);
}
}

const originalConnectedCallback = Cstr.prototype.connectedCallback;
Expand Down
7 changes: 3 additions & 4 deletions src/runtime/bootstrap-lazy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,17 +126,16 @@ export const bootstrapLazy = (lazyBundles: d.LazyBundlesRuntimeData, options: d.
}
};

if (BUILD.cloneNodeFix) {
patchCloneNode(HostElement.prototype);
}

if (
!BUILD.hydrateServerSide && (
(cmpMeta.$flags$ & CMP_FLAGS.hasSlotRelocation) ||
(cmpMeta.$flags$ & (CMP_FLAGS.shadowDomEncapsulation && CMP_FLAGS.needsShadowDomShim))
)
) {
patchPseudoShadowDom(HostElement.prototype);
if (BUILD.cloneNodeFix) {
patchCloneNode(HostElement.prototype);
}
}

if (BUILD.hotModuleReplacement) {
Expand Down
77 changes: 59 additions & 18 deletions src/runtime/dom-extras.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,29 @@ import type * as d from '../declarations';
import { BUILD } from '@app-data';
import { HOST_FLAGS } from '@utils';
import { PLATFORM_FLAGS } from './runtime-constants';
import { plt, supportsShadow, getHostRef } from '@platform';
import { plt, getHostRef } from '@platform';
import { updateFallbackSlotVisibility } from './vdom/render-slot-fallback';

export const patchCloneNode = (HostElementPrototype: any) => {
const orgCloneNode = HostElementPrototype.cloneNode;

HostElementPrototype.cloneNode = function (deep?: boolean) {
const srcNode = this;
const isShadowDom = BUILD.shadowDom ? srcNode.shadowRoot && supportsShadow : false;
const clonedNode = orgCloneNode.call(srcNode, isShadowDom ? deep : false) as Node;
if (BUILD.slot && !isShadowDom && deep) {
const srcNode: d.HostElement = this;
const clonedNode: d.HostElement = orgCloneNode.call(srcNode, false) as d.HostElement;
if (BUILD.slot && deep) {
let i = 0;
let slotted, nonStencilNode;
let stencilPrivates = ['s-id', 's-cr', 's-lr', 's-rc', 's-sc', 's-p', 's-cn', 's-sr', 's-sn', 's-hn', 's-ol', 's-nr', 's-si'];
let stencilPrivates = ['s-id', 's-cr', 's-lr', 's-rc', 's-sc', 's-p', 's-cn', 's-sr', 's-sn', 's-hn', 's-ol', 's-nr', 's-si', 's-sf', 's-sfc', 's-hsf'];

for (; i < srcNode.__childNodes.length; i++) {
slotted = (srcNode.__childNodes[i] as any)['s-nr'];
nonStencilNode = stencilPrivates.every((privateField) => !(srcNode.__childNodes[i] as any)[privateField]);

for (; i < srcNode.childNodes.length; i++) {
slotted = (srcNode.childNodes[i] as any)['s-nr'];
nonStencilNode = stencilPrivates.every((privateField) => !(srcNode.childNodes[i] as any)[privateField]);
if (slotted) {
if (BUILD.appendChildSlotFix && (clonedNode as any).__appendChild) {
(clonedNode as any).__appendChild(slotted.cloneNode(true));
} else {
clonedNode.appendChild(slotted.cloneNode(true));
}
clonedNode.__appendChild(slotted.cloneNode(true));
}
if (nonStencilNode){
clonedNode.appendChild((srcNode.childNodes[i] as any).cloneNode(true));
clonedNode.__appendChild((srcNode.__childNodes[i] as any).cloneNode(true));
}
}
}
Expand Down Expand Up @@ -172,7 +169,35 @@ const patchSlotInnerText = (HostElementPrototype: any) => {
})
};

export const patchNodeRemove = (ElementPrototype: any) => {
if (ElementPrototype.__remove) return;
ElementPrototype.__remove = ElementPrototype.remove || true;
if (document.contains(ElementPrototype.parentNode)) patchNodeRemoveChild(ElementPrototype.parentNode);

ElementPrototype.remove = function(this: Element) {
if (this.parentNode) {
return this.parentNode.removeChild(this);
}
return (this as any).__remove();
}
}

const patchNodeRemoveChild = (ElementPrototype: any) => {
if (ElementPrototype.__removeChild) return;
ElementPrototype.__removeChild = ElementPrototype.removeChild;
ElementPrototype.removeChild = function(this: d.RenderNode, toRemove: d.RenderNode) {
if (toRemove['s-sn']) {
const slotNode = getHostSlotNode((this.__childNodes || this.childNodes), toRemove['s-sn']);
(this as any).__removeChild(toRemove);
if (slotNode && slotNode['s-hsf']) updateFallbackSlotVisibility(this);
return;
}
return (this as any).__removeChild(toRemove);
}
}

const patchSlotAppendChild = (HostElementPrototype: any) => {
if (HostElementPrototype.__appendChild) return;
HostElementPrototype.__appendChild = HostElementPrototype.appendChild;
HostElementPrototype.appendChild = function(this: d.HostElement, newChild: d.RenderNode) {
const slotName = (newChild['s-sn'] = getSlotName(newChild));
Expand All @@ -182,17 +207,23 @@ const patchSlotAppendChild = (HostElementPrototype: any) => {
slotPlaceholder['s-nr'] = newChild;
((slotNode['s-cr']).parentNode as any).__appendChild(slotPlaceholder);
newChild['s-ol'] = slotPlaceholder;
patchNodeRemove(newChild);

const slotChildNodes = getHostSlotChildNodes(slotNode, slotName);
const appendAfter = slotChildNodes[slotChildNodes.length - 1];
return appendAfter.parentNode.insertBefore(newChild, appendAfter.nextSibling);
appendAfter.parentNode.insertBefore(newChild, appendAfter.nextSibling);
patchNodeRemoveChild(newChild.parentNode);

if (slotNode['s-hsf']) updateFallbackSlotVisibility(slotNode.parentNode as d.RenderNode);
return;
}
if (newChild.nodeType === 1 && !!newChild.getAttribute('slot') && this.__childNodes) newChild.hidden = true;
return (this as any).__appendChild(newChild);
};
};

const patchSlotPrepend = (HostElementPrototype: any) => {
if (HostElementPrototype.__prepend) return;
HostElementPrototype.__prepend = HostElementPrototype.prepend;
HostElementPrototype.prepend = function(this: d.HostElement, ...newChildren: (d.RenderNode | string)[]) {
newChildren.forEach((newChild: d.RenderNode | string) => {
Expand All @@ -206,10 +237,15 @@ const patchSlotPrepend = (HostElementPrototype: any) => {
slotPlaceholder['s-nr'] = newChild;
((slotNode['s-cr']).parentNode as any).__appendChild(slotPlaceholder);
newChild['s-ol'] = slotPlaceholder;
patchNodeRemove(newChild);

const slotChildNodes = getHostSlotChildNodes(slotNode, slotName);
const appendAfter = slotChildNodes[0];
return appendAfter.parentNode.insertBefore(newChild, appendAfter.nextSibling);
appendAfter.parentNode.insertBefore(newChild, appendAfter.nextSibling);
patchNodeRemoveChild(newChild.parentNode);

if (slotNode['s-hsf']) updateFallbackSlotVisibility(slotNode.parentNode as d.RenderNode);
return;
}
if (newChild.nodeType === 1 && !!newChild.getAttribute('slot') && this.__childNodes) newChild.hidden = true;
return (this as any).__prepend(newChild);
Expand All @@ -218,6 +254,7 @@ const patchSlotPrepend = (HostElementPrototype: any) => {
};

const patchSlotAppend = (HostElementPrototype: any) => {
if (HostElementPrototype.__append) return;
HostElementPrototype.__append = HostElementPrototype.append;
HostElementPrototype.append = function(this: d.HostElement, ...newChildren: (d.RenderNode | string)[]) {
newChildren.forEach((newChild: d.RenderNode | string) => {
Expand All @@ -230,6 +267,7 @@ const patchSlotAppend = (HostElementPrototype: any) => {
};

const patchSlotReplaceChildren = (HostElementPrototype: any) => {
if (HostElementPrototype.__replaceChildren) return;
HostElementPrototype.__replaceChildren = HostElementPrototype.replaceChildren;
HostElementPrototype.replaceChildren = function(this: d.HostElement, ...newChildren: (Node | string)[]) {
const slotNode = getHostSlotNode(this.__childNodes, '');
Expand All @@ -244,6 +282,7 @@ const patchSlotReplaceChildren = (HostElementPrototype: any) => {
}

const patchSlotInsertAdjacentHTML = (HostElementPrototype: any) => {
if (HostElementPrototype.__insertAdjacentHTML) return;
HostElementPrototype.__insertAdjacentHTML = HostElementPrototype.insertAdjacentHTML;
HostElementPrototype.insertAdjacentHTML = function(this: d.HostElement, position: InsertPosition, text: string) {
if (position !== 'afterbegin' && position !== 'beforeend') {
Expand All @@ -266,13 +305,15 @@ const patchSlotInsertAdjacentHTML = (HostElementPrototype: any) => {
}

const patchSlotInsertAdjacentText = (HostElementPrototype: any) => {
if (HostElementPrototype.__insertAdjacentText) return;
HostElementPrototype.__insertAdjacentText = HostElementPrototype.insertAdjacentText;
HostElementPrototype.insertAdjacentText = function(this: d.HostElement, position: InsertPosition, text: string) {
this.insertAdjacentHTML(position, text);
}
}

const patchSlotInsertAdjacentElement = (HostElementPrototype: any) => {
if (HostElementPrototype.__insertAdjacentElement) return;
HostElementPrototype.__insertAdjacentElement = HostElementPrototype.insertAdjacentElement;
HostElementPrototype.insertAdjacentElement = function(this: d.HostElement, position: InsertPosition, element: d.RenderNode) {
if (position !== 'afterbegin' && position !== 'beforeend') {
Expand Down Expand Up @@ -308,7 +349,7 @@ const getHostSlotNode = (childNodes: NodeListOf<ChildNode>, slotName: string) =>

const getHostSlotChildNodes = (n: d.RenderNode, slotName: string) => {
const childNodes: d.RenderNode[] = [n];
while ((n = n.nextSibling as any) && (n['s-sn'] === slotName || (!n['s-sn'] && slotName === ''))) {
while ((n = n.nextSibling as any) && (n['s-sn'] === slotName)) {
childNodes.push(n as any);
}
return childNodes;
Expand Down
75 changes: 75 additions & 0 deletions src/runtime/vdom/render-slot-fallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type * as d from '../../declarations';
import { NODE_TYPE } from '../runtime-constants';
import { patchNodeRemove } from '../dom-extras';

const renderSlotFallbackContent = (sr: d.RenderNode, hide: boolean) => {
if (!sr['s-hsf']) return;
let n: d.RenderNode = sr;
while ((n = n.previousSibling as d.RenderNode)) {
if (!n['s-sf'] || n['s-sn'] !== sr['s-sn']) continue;
if (n.nodeType === NODE_TYPE.ElementNode) {
n.hidden = hide;
} else if (!!n['s-sfc']) {
if (hide) {
n['s-sfc'] = n.textContent;
n.textContent = '';
} else if (n.textContent.trim() === '') {
n.textContent = n['s-sfc'];
}
}
}
}

export const updateFallbackSlotVisibility = (elm: d.RenderNode) => {
let childNodes: d.RenderNode[] = ((elm as d.RenderNode).__childNodes || elm.childNodes as any);
let childNode: d.RenderNode;
let i: number;
let ilen: number;
let j: number;
let slotNameAttr: string;
let nodeType: number;

for (i = 0, ilen = childNodes.length; i < ilen; i++) {
childNode = childNodes[i];

if (childNode['s-sr']) {
// this is a slot fallback node

// get the slot name for this slot reference node
slotNameAttr = childNode['s-sn'];

// by default always show a fallback slot node
// then hide it if there are other slots in the light dom
renderSlotFallbackContent(childNode, false);

for (j = 0; j < ilen; j++) {
nodeType = childNodes[j].nodeType;

if (childNodes[j]['s-sf']) continue;

if (childNodes[j]['s-hn'] !== childNode['s-hn'] || slotNameAttr !== '') {
// this sibling node is from a different component OR is a named fallback slot node
if (nodeType === NODE_TYPE.ElementNode && slotNameAttr === childNodes[j]['s-sn']) {
renderSlotFallbackContent(childNode, true);
patchNodeRemove(childNodes[j]);
break;
}
} else {
// this is a default fallback slot node
// any element or text node (with content)
// should hide the default fallback slot node
if (
nodeType === NODE_TYPE.ElementNode ||
(nodeType === NODE_TYPE.TextNode && childNodes[j].textContent.trim() !== '')
) {
renderSlotFallbackContent(childNode, true);
patchNodeRemove(childNodes[j]);
break;
}
}
}
}
// keep drilling down
updateFallbackSlotVisibility(childNode);
}
};
19 changes: 9 additions & 10 deletions src/runtime/vdom/test/scoped-slot.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,9 @@ describe('scoped slot', () => {
});

expect(root.firstElementChild.nodeName).toBe('SPIDER');
expect(root.firstElementChild.children).toHaveLength(1);
expect(root.firstElementChild.firstElementChild.nodeName).toBe('SLOT-FB');
expect(root.firstElementChild.firstElementChild.textContent).toBe('default content');
expect(root.firstElementChild.firstElementChild.childNodes).toHaveLength(1);
expect(root.firstElementChild.children).toHaveLength(0);
expect(root.firstElementChild.textContent).toBe('default content');
expect(root.firstElementChild.childNodes).toHaveLength(2);
});

it('should use components default slot node content', async () => {
Expand All @@ -68,8 +67,8 @@ describe('scoped slot', () => {
});

expect(root.firstElementChild.nodeName).toBe('SPIDER');
expect(root.firstElementChild.firstElementChild.nodeName).toBe('SLOT-FB');
expect(root.firstElementChild.firstElementChild.firstElementChild.textContent).toBe('default content');
expect(root.firstElementChild.firstElementChild.nodeName).toBe('DIV');
expect(root.firstElementChild.firstElementChild.textContent).toBe('default content');
});

it('should relocate nested named slot nodes', async () => {
Expand Down Expand Up @@ -774,8 +773,8 @@ describe('scoped slot', () => {
html: `<fallback-test><span>Content</span></fallback-test>`,
});

expect(root.firstElementChild.children[1].nodeName).toBe('SLOT-FB');
expect(root.firstElementChild.children[1]).toHaveAttribute('hidden');
expect(root.firstElementChild.children[1].nodeName).toBe('SPAN');
expect(root.firstElementChild.children[2]).toBe(undefined);
});

it('should hide the slot\'s fallback content for a non-shadow component when slot content passed in', async () => {
Expand All @@ -796,7 +795,7 @@ describe('scoped slot', () => {
html: `<fallback-test><span>Content</span></fallback-test>`,
});

expect(root.firstElementChild.children[1].nodeName).toBe('SLOT-FB');
expect(root.firstElementChild.children[1]).toHaveAttribute('hidden');
expect(root.firstElementChild.children[1].nodeName).toBe('SPAN');
expect(root.firstElementChild.children[2]).toBe(undefined);
});
});

0 comments on commit f9a0063

Please sign in to comment.