Skip to content

Commit

Permalink
feat: new hook useProvideContext
Browse files Browse the repository at this point in the history
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]);
```
  • Loading branch information
cristinecula committed Nov 4, 2022
1 parent 7ac506f commit 4069d4f
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/few-dryers-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"haunted": minor
---

New hook: useProvideContext
1 change: 1 addition & 0 deletions src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export { useState } from './use-state';
export { useReducer } from './use-reducer';
export { useMemo } from './use-memo';
export { useContext } from './use-context';
export { useProvideContext } from './use-provide-context';
export { useRef } from './use-ref';
export { hook, Hook } from './hook';
export { BaseScheduler } from './scheduler';
Expand Down
75 changes: 75 additions & 0 deletions src/use-provide-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Context, ContextDetail } from "./create-context";
import { Hook, hook } from "./hook";
import { State } from "./state";
import { contextEvent } from "./symbols";

/**
* @function
* @template T
* @param {Context} Context Context to provide a value for
* @param {T} value the current value
* @param {unknown[]} values dependencies to the value update
* @return void
*/
export const useProvideContext = hook(
class<T> extends Hook<[Context<T>, T, unknown[]], void, Element> {
listeners: Set<(value: T) => void>;

constructor(
id: number,
state: State<Element>,
private context: Context<T>,
private value: T,
private values?: unknown[]
) {
super(id, state);
this.context = context;
this.value = value;
this.values = values;

this.listeners = new Set();
this.state.host.addEventListener(contextEvent, this);
}

disconnectedCallback() {
this.state.host.removeEventListener(contextEvent, this);
}

handleEvent(event: CustomEvent<ContextDetail<T>>): void {
const { detail } = event;

if (detail.Context === this.context) {
detail.value = this.value;
detail.unsubscribe = this.unsubscribe.bind(this, detail.callback);

this.listeners.add(detail.callback);

event.stopPropagation();
}
}

unsubscribe(callback: (value: T) => void): void {
this.listeners.delete(callback);
}

update(context: Context<T>, value: T, values?: unknown[]): void {
if (this.hasChanged(values)) {
this.values = values;
this.value = value;
for (const callback of this.listeners) {
callback(value);
}
}
}

hasChanged(values?: unknown[]) {
const lastValues = this.values;

if (lastValues == null || values == null) {
return true;
}

return values.some((value, i) => lastValues[i] !== value);
}
}
);
21 changes: 20 additions & 1 deletion test/context.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { component, html, createContext, useContext, useState } from '../src/haunted.js';
import { component, html, createContext, useContext, useState, useProvideContext } from '../src/haunted.js';
import { fixture, expect, nextFrame } from '@open-wc/testing';

describe('context', function() {
Expand Down Expand Up @@ -37,16 +37,25 @@ describe('context', function() {
component(ProviderWithSlots)
);

function CustomProvider(host) {
const {value} = host;
useProvideContext(Context, value, [value]);
}

customElements.define('custom-provider', component(CustomProvider));

let withProviderValue, withProviderUpdate;
let rootProviderValue, rootProviderUpdate;
let nestedProviderValue, nestedProviderUpdate;
let genericConsumerValue, genericConsumerUpdate;
let customProviderValue, customProviderUpdate;

function Tests() {
[withProviderValue, withProviderUpdate] = useState();
[rootProviderValue, rootProviderUpdate] = useState('root');
[nestedProviderValue, nestedProviderUpdate] = useState('nested');
[genericConsumerValue, genericConsumerUpdate] = useState('generic');
[customProviderValue, customProviderUpdate] = useState('custom');

return html`
<div id="without-provider">
Expand Down Expand Up @@ -81,6 +90,12 @@ describe('context', function() {
</slotted-context-provider>
</context-provider>
</div>
<div id="custom-provider">
<custom-provider .value=${customProviderValue}>
<context-consumer></context-consumer>
</custom-provider>
</div>
`;
}

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

it('uses custom value when custom provider is found', async () => {
expect(getResults('#custom-provider context-consumer')[0]).to.equal('custom');
});

describe('with generic consumer component', function () {
it('should render template with context value', async () => {
expect(getContentResults('#generic-consumer generic-consumer')).to.deep.equal(['generic-value']);
Expand Down

0 comments on commit 4069d4f

Please sign in to comment.