Skip to content

Commit

Permalink
fix(slot): fix appendChild when using slot polyfill
Browse files Browse the repository at this point in the history
Closes #1686
  • Loading branch information
adamdbradley committed Dec 31, 2019
1 parent 4070312 commit e8b4c59
Show file tree
Hide file tree
Showing 14 changed files with 240 additions and 27 deletions.
4 changes: 3 additions & 1 deletion src/compiler/app-core/build-conditionals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function getBuildFeatures(cmps: d.ComponentCompilerMeta[]) {
shadowDom,
shadowDelegatesFocus: shadowDom && cmps.some(c => c.shadowDelegatesFocus),
slot,
slotRelocation: slot, // TODO: cmps.some(c => c.htmlTagNames.includes('slot') && c.encapsulation !== 'shadow'),
slotRelocation: slot,
state: cmps.some(c => c.hasState),
style: cmps.some(c => c.hasStyle),
svg: cmps.some(c => c.htmlTagNames.includes('svg')),
Expand All @@ -62,6 +62,7 @@ export function getBuildFeatures(cmps: d.ComponentCompilerMeta[]) {
watchCallback: cmps.some(c => c.hasWatchCallback),
taskQueue: true,
cloneNodeFix: false,
appendChildSlotFix: false,
};
f.asyncLoading = f.cmpWillUpdate || f.cmpWillLoad || f.cmpWillRender;

Expand Down Expand Up @@ -131,6 +132,7 @@ export function updateBuildConditionals(config: d.Config, b: d.Build) {

if (config.extras) {
b.cloneNodeFix = !!config.extras.cloneNodeFix;
b.appendChildSlotFix = b.slotRelocation && !!config.extras.appendChildSlotFix;
}
}

Expand Down
1 change: 1 addition & 0 deletions src/compiler/browser/build-conditionals-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export const BUILD: Required<d.Build> = {
profile: false,
slotRelocation: true,
cloneNodeFix: false,
appendChildSlotFix: false,
};

export const NAMESPACE = 'app';
1 change: 1 addition & 0 deletions src/compiler/component-hydrate/generate-hydrate-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ function getBuildConditionals(config: d.Config, cmps: d.ComponentCompilerMeta[])
build.devTools = false;
build.hotModuleReplacement = false;
build.cloneNodeFix = false;
build.appendChildSlotFix = true;

return build;
}
Expand Down
8 changes: 0 additions & 8 deletions src/compiler/component-hydrate/write-hydrate-outputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,6 @@ async function writeHydrateOutput(config: d.Config, compilerCtx: d.CompilerCtx,
await Promise.all(rollupOutput.output.map(async output => {
if (output.type === 'chunk') {
const filePath = config.sys.path.join(hydrateAppDirPath, output.fileName);
try {
const existingCode = await compilerCtx.fs.disk.readFile(filePath);
if (existingCode === output.code) {
// if it's identical then don't overwrite it so debugging context works
return;
}
} catch (e) {}

await compilerCtx.fs.writeFile(filePath, output.code);
}
}));
Expand Down
1 change: 1 addition & 0 deletions src/compiler/output-targets/output-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ function getBuildConditionals(config: d.Config, cmps: d.ComponentCompilerMeta[])
updateBuildConditionals(config, build);
build.devTools = false;
build.cloneNodeFix = false;
build.appendChildSlotFix = false;

return build;
}
Expand Down
1 change: 1 addition & 0 deletions src/declarations/build-conditionals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export interface BuildFeatures {

// extras
cloneNodeFix: boolean;
appendChildSlotFix: boolean;
}

export interface Build extends Partial<BuildFeatures> {
Expand Down
19 changes: 18 additions & 1 deletion src/declarations/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,13 @@ export interface StencilConfig {
*/
logger?: Logger;

/**
* Config to add extra runtime for DOM features that require more polyfills. Note
* that not all DOM APIs are fully polyfilled when using the slot polyfill. These
* are opt-in since not all users will require the additional runtime.
*/
extras?: ConfigExtras;

globalScript?: string;
srcIndexHtml?: string;
watch?: boolean;
Expand All @@ -191,11 +198,21 @@ export interface StencilConfig {
excludeUnusedDependencies?: boolean;

stencilCoreResolvedId?: string;
extras?: ConfigExtras;
}

export interface ConfigExtras {
/**
* By default, the runtime does not polyfill `cloneNode()` when cloning a component
* that uses the slot polyfill. This is an opt-in polyfill for those who need it.
*/
cloneNodeFix?: boolean;

/**
* By default, the slot polyfill does not update `appendChild()` so that it appends
* new child nodes into the correct child slot like how shadow dom works. This is an opt-in
* polyfill for those who need it.
*/
appendChildSlotFix?: boolean;
}

export interface Config extends StencilConfig {
Expand Down
23 changes: 6 additions & 17 deletions src/runtime/bootstrap-lazy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { hmrStart } from './hmr-component';
import { HYDRATE_ID, PLATFORM_FLAGS, PROXY_FLAGS } from './runtime-constants';
import { appDidLoad, forceUpdate } from './update-component';
import { createTime, installDevTools } from './profile';
import { appendChildSlotFix, cloneNodeFix } from './dom-extras';


export const bootstrapLazy = (lazyBundles: d.LazyBundlesRuntimeData, options: d.CustomElementsDefineOptions = {}) => {
Expand Down Expand Up @@ -137,23 +138,11 @@ export const bootstrapLazy = (lazyBundles: d.LazyBundlesRuntimeData, options: d.
};

if (BUILD.cloneNodeFix) {
const orgCloneNode = HostElement.prototype.cloneNode;
HostElement.prototype.cloneNode = function(deep?: boolean) {
const srcNode = this;
const isShadowDom = BUILD.shadowDom ? srcNode.shadowRoot && supportsShadowDom : false;
const clonedNode = orgCloneNode.call(this, isShadowDom ? deep : false) as Node;
if (BUILD.slot && !isShadowDom && deep) {
let i = 0;
let slotted;
for (; i < srcNode.childNodes.length; i++) {
slotted = (srcNode.childNodes[i] as any)['s-nr'];
if (slotted) {
clonedNode.appendChild(slotted.cloneNode(true));
}
}
}
return clonedNode;
};
cloneNodeFix(HostElement.prototype);
}

if (BUILD.appendChildSlotFix) {
appendChildSlotFix(HostElement.prototype);
}

if (BUILD.hotModuleReplacement) {
Expand Down
74 changes: 74 additions & 0 deletions src/runtime/dom-extras.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import * as d from '../declarations';
import { BUILD } from '@build-conditionals';
import { supportsShadowDom } from '@platform';


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

HostElementPrototype.cloneNode = function(deep?: boolean) {
const srcNode = this;
const isShadowDom = BUILD.shadowDom ? srcNode.shadowRoot && supportsShadowDom : false;
const clonedNode = orgCloneNode.call(srcNode, isShadowDom ? deep : false) as Node;
if (BUILD.slot && !isShadowDom && deep) {
let i = 0;
let slotted;
for (; i < srcNode.childNodes.length; i++) {
slotted = (srcNode.childNodes[i] as any)['s-nr'];
if (slotted) {
if (BUILD.appendChildSlotFix && (clonedNode as any).__appendChild) {
(clonedNode as any).__appendChild(slotted.cloneNode(true));
} else {
clonedNode.appendChild(slotted.cloneNode(true));
}
}
}
}
return clonedNode;
};
};

export const appendChildSlotFix = (HostElementPrototype: any) => {

HostElementPrototype.__appendChild = HostElementPrototype.appendChild;
HostElementPrototype.appendChild = function(this: d.RenderNode, newChild: d.RenderNode) {
const slotName = newChild['s-sn'] = getSlotName(newChild);
const slotNode = getHostSlotNode(this, slotName);
if (slotNode) {
const slotChildNodes = getHostSlotChildNodes(slotNode, slotName);
const appendAfter = slotChildNodes[slotChildNodes.length - 1];
return appendAfter.parentNode.insertBefore(newChild, appendAfter.nextSibling);
}
return (this as any).__appendChild(newChild);
};

};

const getSlotName = (node: Node) =>
(node.nodeType === 1 && (node as Element).getAttribute('slot')) || '';

const getHostSlotNode = (elm: d.RenderNode, slotName: string) => {
let childNodes = elm.childNodes as any as d.RenderNode[];
let i = 0;
let childNode: d.RenderNode;

for (; i < childNodes.length; i++) {
childNode = childNodes[i];
if (childNode['s-sr'] && childNode['s-sn'] === slotName) {
return childNode;
}
childNode = getHostSlotNode(childNode, slotName);
if (childNode) {
return childNode;
}
}
return null;
};

const getHostSlotChildNodes = (n: d.RenderNode, slotName: string) => {
const childNodes: d.RenderNode[] = [n];
while ((n = n.nextSibling as any) && (n as d.RenderNode)['s-sn'] === slotName) {
childNodes.push(n as any);
}
return childNodes;
};
1 change: 1 addition & 0 deletions test/karma/stencil.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const config: Config = {
],
extras: {
cloneNodeFix: true,
appendChildSlotFix: true,
},
_lifecycleDOMEvents: true,
devServer: {
Expand Down
50 changes: 50 additions & 0 deletions test/karma/test-app/append-child/cmp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Component, Host, h } from '@stencil/core';

@Component({
tag: 'append-child',
styles: `
h1 {
color: red;
font-weight: bold;
}
article {
color: green;
font-weight: bold;
}
section {
color: blue;
font-weight: bold;
}
`,
scoped: true,
})
export class AppendChild {

render() {
return (
<Host>
<h1>
H1 Top
<slot name="h1"/>
<div>
H1 Bottom
</div>
</h1>
<article>
Default Top
<slot/>
Default Bottom
</article>
<h6>
<section>
H6 Top
<slot name="h6"/>
<div>
H6 Bottom
</div>
</section>
</h6>
</Host>
);
}
}
40 changes: 40 additions & 0 deletions test/karma/test-app/append-child/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<meta charset="utf8">
<script src="/build/testapp.esm.js" type="module"></script>
<script src="/build/testapp.js" nomodule></script>

<button id="btnDefault" onclick="appendToDefault()">Append To Default Slot</button>
<br>
<button id="btnH1" onclick="appendToH1()">Append To H1 Slot</button id="btnDefault">
<br>
<button id="btnH6" onclick="appendToH6()">Append To H6 Slot</button id="btnDefault">
<br>

<append-child id="appendCmp">LightDom</append-child>

<script>
var wc = document.getElementsByTagName('append-child')[0];
var defaultCount = 0;
var h1Count = 0;
var h6Count = 0;

function appendToDefault() {
var defaultElm = document.createElement('nav');
defaultElm.innerHTML = 'Default Slot ' + (defaultCount++);
wc.appendChild(defaultElm);
}

function appendToH1() {
var h1Elm = document.createElement('nav');
h1Elm.setAttribute('slot', 'h1');
h1Elm.innerHTML = 'H1 Middle ' + (h1Count++);
wc.appendChild(h1Elm);
}

function appendToH6() {
var h6Elm = document.createElement('nav');
h6Elm.setAttribute('slot', 'h6');
h6Elm.innerHTML = 'H6 Middle ' + (h6Count++);
wc.appendChild(h6Elm);
}
</script>
33 changes: 33 additions & 0 deletions test/karma/test-app/append-child/karma.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { setupDomTests, waitForChanges } from '../util';


describe('append-child', function() {
const { setupDom, tearDownDom } = setupDomTests(document);
let app: HTMLElement;

beforeEach(async () => {
app = await setupDom('/append-child/index.html');
});
afterEach(tearDownDom);

it('appends to correct slot', async () => {
const btnDefault = app.querySelector('#btnDefault') as HTMLButtonElement;
btnDefault.click();
btnDefault.click();

const btnH1 = app.querySelector('#btnH1') as HTMLButtonElement;
btnH1.click();
btnH1.click();

const btnH6 = app.querySelector('#btnH6') as HTMLButtonElement;
btnH6.click();
btnH6.click();

await waitForChanges();

expect(app.querySelector('h1').textContent).toBe('H1 TopH1 Middle 0H1 Middle 1H1 Bottom');
expect(app.querySelector('article').textContent).toBe('Default TopLightDomDefault Slot 0Default Slot 1Default Bottom');
expect(app.querySelector('section').textContent).toBe('H6 TopH6 Middle 0H6 Middle 1H6 Bottom');
});

});

0 comments on commit e8b4c59

Please sign in to comment.