Skip to content

Commit 4069d4f

Browse files
committed
feat: new hook useProvideContext
This hook enables you to provide values to one or multiple contexts from the same component. Instead of: ```html <app-state-provider .value=${appState}> <settings-provider .value=${settings}> <main-app></main-app> </settings-provider> </app-state-provider> ``` you can do: ```js useProvideContext(AppStateContext, appState, [appState]); useProvideContext(SettingsContext, settings, [settings]); ```
1 parent 7ac506f commit 4069d4f

File tree

4 files changed

+101
-1
lines changed

4 files changed

+101
-1
lines changed

.changeset/few-dryers-wait.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"haunted": minor
3+
---
4+
5+
New hook: useProvideContext

src/core.ts

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export { useState } from './use-state';
3333
export { useReducer } from './use-reducer';
3434
export { useMemo } from './use-memo';
3535
export { useContext } from './use-context';
36+
export { useProvideContext } from './use-provide-context';
3637
export { useRef } from './use-ref';
3738
export { hook, Hook } from './hook';
3839
export { BaseScheduler } from './scheduler';

src/use-provide-context.ts

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Context, ContextDetail } from "./create-context";
2+
import { Hook, hook } from "./hook";
3+
import { State } from "./state";
4+
import { contextEvent } from "./symbols";
5+
6+
/**
7+
* @function
8+
* @template T
9+
* @param {Context} Context Context to provide a value for
10+
* @param {T} value the current value
11+
* @param {unknown[]} values dependencies to the value update
12+
* @return void
13+
*/
14+
export const useProvideContext = hook(
15+
class<T> extends Hook<[Context<T>, T, unknown[]], void, Element> {
16+
listeners: Set<(value: T) => void>;
17+
18+
constructor(
19+
id: number,
20+
state: State<Element>,
21+
private context: Context<T>,
22+
private value: T,
23+
private values?: unknown[]
24+
) {
25+
super(id, state);
26+
this.context = context;
27+
this.value = value;
28+
this.values = values;
29+
30+
this.listeners = new Set();
31+
this.state.host.addEventListener(contextEvent, this);
32+
}
33+
34+
disconnectedCallback() {
35+
this.state.host.removeEventListener(contextEvent, this);
36+
}
37+
38+
handleEvent(event: CustomEvent<ContextDetail<T>>): void {
39+
const { detail } = event;
40+
41+
if (detail.Context === this.context) {
42+
detail.value = this.value;
43+
detail.unsubscribe = this.unsubscribe.bind(this, detail.callback);
44+
45+
this.listeners.add(detail.callback);
46+
47+
event.stopPropagation();
48+
}
49+
}
50+
51+
unsubscribe(callback: (value: T) => void): void {
52+
this.listeners.delete(callback);
53+
}
54+
55+
update(context: Context<T>, value: T, values?: unknown[]): void {
56+
if (this.hasChanged(values)) {
57+
this.values = values;
58+
this.value = value;
59+
for (const callback of this.listeners) {
60+
callback(value);
61+
}
62+
}
63+
}
64+
65+
hasChanged(values?: unknown[]) {
66+
const lastValues = this.values;
67+
68+
if (lastValues == null || values == null) {
69+
return true;
70+
}
71+
72+
return values.some((value, i) => lastValues[i] !== value);
73+
}
74+
}
75+
);

test/context.test.ts

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { component, html, createContext, useContext, useState } from '../src/haunted.js';
1+
import { component, html, createContext, useContext, useState, useProvideContext } from '../src/haunted.js';
22
import { fixture, expect, nextFrame } from '@open-wc/testing';
33

44
describe('context', function() {
@@ -37,16 +37,25 @@ describe('context', function() {
3737
component(ProviderWithSlots)
3838
);
3939

40+
function CustomProvider(host) {
41+
const {value} = host;
42+
useProvideContext(Context, value, [value]);
43+
}
44+
45+
customElements.define('custom-provider', component(CustomProvider));
46+
4047
let withProviderValue, withProviderUpdate;
4148
let rootProviderValue, rootProviderUpdate;
4249
let nestedProviderValue, nestedProviderUpdate;
4350
let genericConsumerValue, genericConsumerUpdate;
51+
let customProviderValue, customProviderUpdate;
4452

4553
function Tests() {
4654
[withProviderValue, withProviderUpdate] = useState();
4755
[rootProviderValue, rootProviderUpdate] = useState('root');
4856
[nestedProviderValue, nestedProviderUpdate] = useState('nested');
4957
[genericConsumerValue, genericConsumerUpdate] = useState('generic');
58+
[customProviderValue, customProviderUpdate] = useState('custom');
5059

5160
return html`
5261
<div id="without-provider">
@@ -81,6 +90,12 @@ describe('context', function() {
8190
</slotted-context-provider>
8291
</context-provider>
8392
</div>
93+
94+
<div id="custom-provider">
95+
<custom-provider .value=${customProviderValue}>
96+
<context-consumer></context-consumer>
97+
</custom-provider>
98+
</div>
8499
`;
85100
}
86101

@@ -122,6 +137,10 @@ describe('context', function() {
122137
expect(getResults('#with-slotted-provider slotted-context-provider context-consumer')[0]).to.equal('slotted');
123138
});
124139

140+
it('uses custom value when custom provider is found', async () => {
141+
expect(getResults('#custom-provider context-consumer')[0]).to.equal('custom');
142+
});
143+
125144
describe('with generic consumer component', function () {
126145
it('should render template with context value', async () => {
127146
expect(getContentResults('#generic-consumer generic-consumer')).to.deep.equal(['generic-value']);

0 commit comments

Comments
 (0)