Skip to content

Commit 52fe70b

Browse files
fix: Pass context value across custom element boundaries
1 parent a3bdfed commit 52fe70b

File tree

2 files changed

+71
-13
lines changed

2 files changed

+71
-13
lines changed

src/index.js

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,32 @@ export default function register(Component, tagName, propNames, options) {
2424
);
2525
}
2626

27+
function ContextProvider(props) {
28+
this.getChildContext = () => props.context;
29+
// eslint-disable-next-line no-unused-vars
30+
const { context, children, ...rest } = props;
31+
return cloneElement(children, rest);
32+
}
33+
2734
function connectedCallback() {
28-
this._vdom = toVdom(this, this._vdomComponent);
35+
// Obtain a reference to the previous context by pinging the nearest
36+
// higher up node that was rendered with Preact. If one Preact component
37+
// higher up receives our ping, it will set the `detail` property of
38+
// our custom event. This works because events are dispatched
39+
// synchronously.
40+
const event = new CustomEvent('_preact', {
41+
detail: {},
42+
bubbles: true,
43+
cancelable: true,
44+
});
45+
this.dispatchEvent(event);
46+
const context = event.detail.context;
47+
48+
this._vdom = h(
49+
ContextProvider,
50+
{ context },
51+
toVdom(this, true, this._vdomComponent)
52+
);
2953
(this.hasAttribute('hydrate') ? hydrate : render)(this._vdom, this._root);
3054
}
3155

@@ -46,7 +70,32 @@ function disconnectedCallback() {
4670
render((this._vdom = null), this._root);
4771
}
4872

49-
function toVdom(element, nodeName) {
73+
/**
74+
* Pass an event listener to each `<slot>` that "forwards" the current
75+
* context value to the rendered child. The child will trigger a custom
76+
* event, where will add the context value to. Because events work
77+
* synchronously, the child can immediately pull of the value right
78+
* after having fired the event.
79+
*/
80+
function Slot(props, context) {
81+
const ref = (r) => {
82+
if (!r) {
83+
this.ref.removeEventListener('_preact', this._listener);
84+
} else {
85+
this.ref = r;
86+
if (!this._listener) {
87+
this._listener = (event) => {
88+
event.stopPropagation();
89+
event.detail.context = context;
90+
};
91+
r.addEventListener('_preact', this._listener);
92+
}
93+
}
94+
};
95+
return h('slot', { ...props, ref });
96+
}
97+
98+
function toVdom(element, wrap, nodeName) {
5099
if (element.nodeType === 3) return element.data;
51100
if (element.nodeType !== 1) return null;
52101
let children = [],
@@ -62,14 +111,17 @@ function toVdom(element, nodeName) {
62111
}
63112

64113
for (i = cn.length; i--; ) {
65-
const vnode = toVdom(cn[i]);
114+
const vnode = toVdom(cn[i], false);
66115
// Move slots correctly
67116
const name = cn[i].slot;
68117
if (name) {
69-
props[name] = h('slot', { name }, vnode);
118+
props[name] = h(Slot, { name }, vnode);
70119
} else {
71120
children[i] = vnode;
72121
}
73122
}
74-
return h(nodeName || element.nodeName.toLowerCase(), props, children);
123+
124+
// Only wrap the topmost node with a slot
125+
const wrappedChildren = wrap ? h(Slot, null, children) : children;
126+
return h(nodeName || element.nodeName.toLowerCase(), props, wrappedChildren);
75127
}

src/index.test.jsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { assert } from '@open-wc/testing';
22
import { h, createContext } from 'preact';
33
import { useContext } from 'preact/hooks';
4+
import { act } from 'preact/test-utils';
45
import registerElement from './index';
56

67
function Clock({ time }) {
@@ -70,7 +71,7 @@ it('renders slots as props with shadow DOM', () => {
7071
const shadowHTML = document.querySelector('x-foo').shadowRoot.innerHTML;
7172
assert.equal(
7273
shadowHTML,
73-
'<span class="wrapper"><div class="children"><div>no slot</div></div><div class="slotted"><slot name="text"><span>here is a slot</span></slot></div></span>'
74+
'<span class="wrapper"><div class="children"><slot><div>no slot</div></slot></div><div class="slotted"><slot name="text"><span>here is a slot</span></slot></div></span>'
7475
);
7576

7677
document.body.removeChild(root);
@@ -124,17 +125,17 @@ function DisplayTheme() {
124125

125126
registerElement(DisplayTheme, 'x-display-theme', [], { shadow: true });
126127

127-
function Parent({ children }) {
128+
function Parent({ children, theme = 'dark' }) {
128129
return (
129-
<Theme.Provider value="dark">
130+
<Theme.Provider value={theme}>
130131
<div class="children">{children}</div>
131132
</Theme.Provider>
132133
);
133134
}
134135

135-
registerElement(Parent, 'x-parent', [], { shadow: true });
136+
registerElement(Parent, 'x-parent', ['theme'], { shadow: true });
136137

137-
it('passes down information using context over custom element boundaries', () => {
138+
it('passes context over custom element boundaries', async () => {
138139
const root = document.createElement('div');
139140
const el = document.createElement('x-parent');
140141

@@ -149,10 +150,15 @@ it('passes down information using context over custom element boundaries', () =>
149150
'<x-parent><x-display-theme></x-display-theme></x-parent>'
150151
);
151152

152-
const shadowHTML = document.querySelector('x-display-theme').shadowRoot
153-
.innerHTML;
153+
const getShadowHTML = () =>
154+
document.querySelector('x-display-theme').shadowRoot.innerHTML;
155+
assert.equal(getShadowHTML(), '<p>Active theme: dark</p>');
154156

155-
assert.equal(shadowHTML, '<p>Active theme: dark</p>');
157+
// Trigger context update
158+
act(() => {
159+
el.setAttribute('theme', 'sunny');
160+
});
161+
assert.equal(getShadowHTML(), '<p>Active theme: sunny</p>');
156162

157163
document.body.removeChild(root);
158164
});

0 commit comments

Comments
 (0)