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 e285f9c
Show file tree
Hide file tree
Showing 4 changed files with 98 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
72 changes: 72 additions & 0 deletions src/use-provide-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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> {
Context!: Context<T>;
value!: T;
values: unknown[];
listeners: Set<(value: T) => void>;

constructor(
id: number,
state: State<Element>,
Context: Context<T>,
value: T,
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[] = []) {
return values.some((value, i) => this.values[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 e285f9c

Please sign in to comment.