Skip to content

Commit f955bc3

Browse files
Merge pull request #43 from preactjs/context-slot
2 parents 1beca66 + 52fe70b commit f955bc3

File tree

2 files changed

+108
-7
lines changed

2 files changed

+108
-7
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: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { assert } from '@open-wc/testing';
2-
import { h } from 'preact';
2+
import { h, createContext } from 'preact';
3+
import { useContext } from 'preact/hooks';
4+
import { act } from 'preact/test-utils';
35
import registerElement from './index';
46

57
function Clock({ time }) {
@@ -69,7 +71,7 @@ it('renders slots as props with shadow DOM', () => {
6971
const shadowHTML = document.querySelector('x-foo').shadowRoot.innerHTML;
7072
assert.equal(
7173
shadowHTML,
72-
'<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>'
7375
);
7476

7577
document.body.removeChild(root);
@@ -113,3 +115,50 @@ it('handles kebab-case attributes with passthrough', () => {
113115

114116
document.body.removeChild(root);
115117
});
118+
119+
const Theme = createContext('light');
120+
121+
function DisplayTheme() {
122+
const theme = useContext(Theme);
123+
return <p>Active theme: {theme}</p>;
124+
}
125+
126+
registerElement(DisplayTheme, 'x-display-theme', [], { shadow: true });
127+
128+
function Parent({ children, theme = 'dark' }) {
129+
return (
130+
<Theme.Provider value={theme}>
131+
<div class="children">{children}</div>
132+
</Theme.Provider>
133+
);
134+
}
135+
136+
registerElement(Parent, 'x-parent', ['theme'], { shadow: true });
137+
138+
it('passes context over custom element boundaries', async () => {
139+
const root = document.createElement('div');
140+
const el = document.createElement('x-parent');
141+
142+
const noSlot = document.createElement('x-display-theme');
143+
el.appendChild(noSlot);
144+
145+
root.appendChild(el);
146+
document.body.appendChild(root);
147+
148+
assert.equal(
149+
root.innerHTML,
150+
'<x-parent><x-display-theme></x-display-theme></x-parent>'
151+
);
152+
153+
const getShadowHTML = () =>
154+
document.querySelector('x-display-theme').shadowRoot.innerHTML;
155+
assert.equal(getShadowHTML(), '<p>Active theme: dark</p>');
156+
157+
// Trigger context update
158+
act(() => {
159+
el.setAttribute('theme', 'sunny');
160+
});
161+
assert.equal(getShadowHTML(), '<p>Active theme: sunny</p>');
162+
163+
document.body.removeChild(root);
164+
});

0 commit comments

Comments
 (0)