Skip to content

Provide light dom props children #79

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
65 changes: 56 additions & 9 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,39 @@
import { h, cloneElement, render, hydrate } from 'preact';
import { h, cloneElement, render, hydrate, Fragment } from 'preact';

// This function is used to register a component with the given tag name.
export default function register(Component, tagName, propNames, options) {
// Create an instance of PreactElement, which extends HTMLElement.
function PreactElement() {
const inst = Reflect.construct(HTMLElement, [], PreactElement);
inst._vdomComponent = Component;

// If options for shadow DOM are given, attach a shadow root.
// Otherwise, assign the instance itself as root.
inst._root =
options && options.shadow ? inst.attachShadow({ mode: 'open' }) : inst;

return inst;
}

// Extend the PreactElement from HTMLElement.
PreactElement.prototype = Object.create(HTMLElement.prototype);
PreactElement.prototype.constructor = PreactElement;
PreactElement.prototype.connectedCallback = connectedCallback;

// Assign lifecycle methods to the PreactElement.
PreactElement.prototype.connectedCallback = function () {
connectedCallback.call(this, options);
};
PreactElement.prototype.attributeChangedCallback = attributeChangedCallback;
PreactElement.prototype.disconnectedCallback = disconnectedCallback;

// Assign observed attributes.
propNames =
propNames ||
Component.observedAttributes ||
Object.keys(Component.propTypes || {});
PreactElement.observedAttributes = propNames;

// Keep DOM properties and Preact props in sync
// Sync DOM properties and Preact props.
propNames.forEach((name) => {
Object.defineProperty(PreactElement.prototype, name, {
get() {
Expand All @@ -35,7 +48,7 @@ export default function register(Component, tagName, propNames, options) {
this.connectedCallback();
}

// Reflect property changes to attributes if the value is a primitive
// Reflect property changes to attributes if the value is a primitive.
const type = typeof v;
if (
v == null ||
Expand All @@ -49,20 +62,25 @@ export default function register(Component, tagName, propNames, options) {
});
});

// Define the custom element.
return customElements.define(
tagName || Component.tagName || Component.displayName || Component.name,
PreactElement
);
}

// The rest of the functions are utility functions used within the register function.

// This function provides the context for child components.
function ContextProvider(props) {
this.getChildContext = () => props.context;
// eslint-disable-next-line no-unused-vars
const { context, children, ...rest } = props;
return cloneElement(children, rest);
}

function connectedCallback() {
// This function is called when the custom element is inserted into the DOM
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 @@ -79,7 +97,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 @@ -113,6 +131,7 @@ function disconnectedCallback() {
* synchronously, the child can immediately pull of the value right
* after having fired the event.
*/
// This function provides a slot for context propagation.
function Slot(props, context) {
const ref = (r) => {
if (!r) {
Expand All @@ -131,7 +150,27 @@ function Slot(props, context) {
return h('slot', { ...props, ref });
}

function toVdom(element, nodeName) {
// This function provides a pseudo-slot for context propagation without shadow dom.
function PseudoSlot(props, context) {
const ref = (r) => {
if (!r) {
this.ref.removeEventListener('_preact', this._listener);
} else {
this.ref = r;
if (!this._listener) {
this._listener = (event) => {
event.stopPropagation();
event.detail.context = context;
};
r.addEventListener('_preact', this._listener);
}
}
};
return h(Fragment, { ...props, ref });
}

// This function converts DOM elements to virtual DOM.
function toVdom(element, nodeName, options) {
if (element.nodeType === 3) return element.data;
if (element.nodeType !== 1) return null;
let children = [],
Expand All @@ -147,7 +186,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 @@ -158,6 +197,14 @@ function toVdom(element, nodeName) {
}

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

const wrappedChildren = nodeName
? h(options && options.shadow === false ? PseudoSlot : Slot, null, children)
: children;

// Remove all children from the topmost node in non-shadow mode
if (options && options.shadow === false && nodeName) {
element.innerHTML = '';
}
return h(nodeName || element.nodeName.toLowerCase(), props, wrappedChildren);
}
56 changes: 55 additions & 1 deletion src/index.test.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { assert } from '@open-wc/testing';
import { h, createContext } from 'preact';
import { h, createContext, Fragment } from 'preact';
import { useContext } from 'preact/hooks';
import { act } from 'preact/test-utils';
import registerElement from './index';
Expand Down Expand Up @@ -245,4 +245,58 @@ describe('web components', () => {
});
assert.equal(getShadowHTML(), '<p>Active theme: sunny</p>');
});

it('renders my-foo with child element correctly', () => {
function FooComponent(props) {
return (
<Fragment>
<h1>My Heading</h1>
<div>{props.children}</div>
</Fragment>
);
}

registerElement(FooComponent, 'my-foo', [], { shadow: false });

const el = document.createElement('my-foo');

const specialElement = document.createElement(
'some-special-custom-element'
);
specialElement.textContent = 'Lorem doFoo';
el.appendChild(specialElement);

root.appendChild(el);
assert.equal(
root.innerHTML,
'<my-foo><h1>My Heading</h1><div><some-special-custom-element>Lorem doFoo</some-special-custom-element></div></my-foo>'
);
});

it('renders my-foo with child element in shadow dom with slot', () => {
function FooComponent(props) {
return (
<Fragment>
<h1>My Heading</h1>
<div>{props.children}</div>
</Fragment>
);
}

registerElement(FooComponent, 'my-foo-shadow', [], { shadow: true });

const el = document.createElement('my-foo-shadow');

const specialElement = document.createElement(
'some-special-custom-element'
);
specialElement.textContent = 'Lorem doFoo';
el.appendChild(specialElement);

root.appendChild(el);
assert.equal(
el.shadowRoot.innerHTML,
'<h1>My Heading</h1><div><slot><some-special-custom-element>Lorem doFoo</some-special-custom-element></slot></div>'
);
});
});