Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { h, cloneElement, render, hydrate } from 'preact';
import { h, cloneElement, render, hydrate, Fragment } from 'preact';

/**
* @typedef {import('./index.d.ts').PreactCustomElement} PreactCustomElement
Expand Down Expand Up @@ -26,7 +26,9 @@ export default function register(Component, tagName, propNames, options) {
}
PreactElement.prototype = Object.create(HTMLElement.prototype);
PreactElement.prototype.constructor = PreactElement;
PreactElement.prototype.connectedCallback = connectedCallback;
PreactElement.prototype.connectedCallback = function () {
connectedCallback.call(this, options);
};
PreactElement.prototype.attributeChangedCallback = attributeChangedCallback;
PreactElement.prototype.disconnectedCallback = disconnectedCallback;

Expand Down Expand Up @@ -89,7 +91,7 @@ function ContextProvider(props) {
/**
* @this {PreactCustomElement}
*/
function connectedCallback() {
function connectedCallback(options) {
// Obtain a reference to the previous context by pinging the nearest
// higher up node that was rendered with Preact. If one Preact component
// higher up receives our ping, it will set the `detail` property of
Expand All @@ -106,7 +108,7 @@ function connectedCallback() {
this._vdom = h(
ContextProvider,
{ ...this._props, context },
toVdom(this, this._vdomComponent)
toVdom(this, this._vdomComponent, options)
);
(this.hasAttribute('hydrate') ? hydrate : render)(this._vdom, this._root);
}
Expand Down Expand Up @@ -170,10 +172,11 @@ function Slot(props, context) {
}
}
};
return h('slot', { ...props, ref });
const { useFragment, ...rest } = props;
return h(useFragment ? Fragment : 'slot', { ...rest, ref });
}

function toVdom(element, nodeName) {
function toVdom(element, nodeName, options) {
if (element.nodeType === 3) return element.data;
if (element.nodeType !== 1) return null;
let children = [],
Expand All @@ -189,7 +192,7 @@ function toVdom(element, nodeName) {
}

for (i = cn.length; i--; ) {
const vnode = toVdom(cn[i], null);
const vnode = toVdom(cn[i], null, options);
// Move slots correctly
const name = cn[i].slot;
if (name) {
Expand All @@ -199,7 +202,15 @@ function toVdom(element, nodeName) {
}
}

const shadow = !!(options && options.shadow);

// Only wrap the topmost node with a slot
const wrappedChildren = nodeName ? h(Slot, null, children) : children;
const wrappedChildren = nodeName
? h(Slot, { useFragment: !shadow }, children)
: children;

if (!shadow && nodeName) {
element.innerHTML = '';
}
return h(nodeName || element.nodeName.toLowerCase(), props, wrappedChildren);
}
64 changes: 63 additions & 1 deletion src/index.test.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import { assert } from '@open-wc/testing';
import { h, createContext, Component } from 'preact';
import { h, createContext, Component, Fragment } from 'preact';
import { useContext } from 'preact/hooks';
import { act } from 'preact/test-utils';
import registerElement from './index';

/** @param {string} name */
function createTestElement(name) {
const el = document.createElement(name);
const child1 = document.createElement('p');
child1.textContent = 'Child 1';
const child2 = document.createElement('p');
child2.textContent = 'Child 2';
el.appendChild(child1);
el.appendChild(child2);
return el;
}

describe('web components', () => {
/** @type {HTMLDivElement} */
let root;
Expand Down Expand Up @@ -359,4 +371,54 @@ describe('web components', () => {
const style = getComputedStyle(child);
assert.equal(style.color, 'rgb(255, 0, 0)');
});

it('supports controlling light DOM children', async () => {
function LightDomChildren({ children }) {
return (
<Fragment>
<h1>Light DOM Children</h1>
<div>{children}</div>
</Fragment>
);
}

registerElement(LightDomChildren, 'light-dom-children', []);
registerElement(LightDomChildren, 'light-dom-children-shadow-false', [], {
shadow: false,
});

root.appendChild(createTestElement('light-dom-children'));
root.appendChild(createTestElement('light-dom-children-shadow-false'));

assert.equal(
document.querySelector('light-dom-children').innerHTML,
'<h1>Light DOM Children</h1><div><p>Child 1</p><p>Child 2</p></div>'
);
assert.equal(
document.querySelector('light-dom-children-shadow-false').innerHTML,
'<h1>Light DOM Children</h1><div><p>Child 1</p><p>Child 2</p></div>'
);
});

it('supports controlling shadow DOM children', () => {
function ShadowDomChildren({ children }) {
return (
<Fragment>
<h1>Light DOM Children</h1>
<div>{children}</div>
</Fragment>
);
}

registerElement(ShadowDomChildren, 'shadow-dom-children', [], {
shadow: true,
});

root.appendChild(createTestElement('shadow-dom-children'));

assert.equal(
document.querySelector('shadow-dom-children').shadowRoot.innerHTML,
'<h1>Light DOM Children</h1><div><slot><p>Child 1</p><p>Child 2</p></slot></div>'
);
});
});