Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new hook useProvideContext #452

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be documented in readme and on the docs site

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bennypowers I have added documentation for the hook.
Do I have to also update custom-elements.json (with the results of npm run analyze)?

1 change: 1 addition & 0 deletions docs/docs/hooks/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Currently Haunted supports the following hooks:
- [useEffect](./useEffect/)
- [useLayoutEffect](./useLayoutEffect/)
- [useMemo](./useMemo/)
- [useProvideContext](./useProvideContext/)
- [useReducer](./useReducer/)
- [useRef](./useRef/)
- [useState](./useState/)
63 changes: 63 additions & 0 deletions docs/docs/hooks/useProvideContext.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
layout: layout-api
package: haunted
module: lib/use-provide-context.js
---

# Hooks >> useProvideContext

Makes your component become a Context provider. It updates every nested consumer with the current value. You can optionally use a deps array (useful when the context value is mutated instead of being replaced).

```js playground use-provide-context use-provide-context.js
import {
html,
component,
useState,
useContext,
createContext,
useProvideContext,
} from "haunted";

const ThemeContext = createContext("dark");

customElements.define("theme-consumer", ThemeContext.Consumer);

function Consumer() {
const context = useContext(ThemeContext);
return context;
}

customElements.define("my-consumer", component(Consumer));

function App() {
const [theme, setTheme] = useState("light");
useProvideContext(ThemeContext, theme);

return html`
<select value=${theme} @change=${(event) => setTheme(event.target.value)}>
<option value="dark">Dark</option>
<option value="light">Light</option>
</select>

<my-consumer></my-consumer>

<theme-provider .value=${theme === "dark" ? "light" : "dark"}>
<theme-consumer
.render=${(value) =>
html`
<h1>${value}</h1>
`}
></theme-consumer>
</theme-provider>
`;
}

customElements.define("use-provide-context", component(App));
```

```html playground-file use-provide-context index.html
<script type="module" src="use-provide-context.js"></script>
<use-provide-context></use-provide-context>
```

## API
1 change: 1 addition & 0 deletions docs/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
- [useEffect](./hooks/useEffect/)
- [useLayoutEffect](./hooks/useLayoutEffect/)
- [useMemo](./hooks/useMemo/)
- [useProvideContext](./hooks/useProvideContext/)
- [useReducer](./hooks/useReducer/)
- [useRef](./hooks/useRef/)
- [useState](./hooks/useState/)
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Currently Haunted supports the following hooks:
- [useEffect](https://hauntedhooks.netlify.app/docs/hooks/useEffect/)
- [useLayoutEffect](https://hauntedhooks.netlify.app/docs/hooks/useLayoutEffect/)
- [useMemo](https://hauntedhooks.netlify.app/docs/hooks/useMemo/)
- [useProvideContext](https://hauntedhooks.netlify.app/docs/hooks/useProvideContext/)
- [useReducer](https://hauntedhooks.netlify.app/docs/hooks/useReducer/)
- [useRef](https://hauntedhooks.netlify.app/docs/hooks/useRef/)
- [useState](https://hauntedhooks.netlify.app/docs/hooks/useState/)
Expand Down
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