From 4069d4f6931cf6891f565f0e06b6ffedf43a6e5c Mon Sep 17 00:00:00 2001 From: Cristian Necula Date: Fri, 4 Nov 2022 03:36:29 +0200 Subject: [PATCH 1/2] feat: new hook useProvideContext This hook enables you to provide values to one or multiple contexts from the same component. Instead of: ```html ``` you can do: ```js useProvideContext(AppStateContext, appState, [appState]); useProvideContext(SettingsContext, settings, [settings]); ``` --- .changeset/few-dryers-wait.md | 5 +++ src/core.ts | 1 + src/use-provide-context.ts | 75 +++++++++++++++++++++++++++++++++++ test/context.test.ts | 21 +++++++++- 4 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 .changeset/few-dryers-wait.md create mode 100644 src/use-provide-context.ts diff --git a/.changeset/few-dryers-wait.md b/.changeset/few-dryers-wait.md new file mode 100644 index 0000000..9630c2e --- /dev/null +++ b/.changeset/few-dryers-wait.md @@ -0,0 +1,5 @@ +--- +"haunted": minor +--- + +New hook: useProvideContext diff --git a/src/core.ts b/src/core.ts index 3f970c1..403e822 100644 --- a/src/core.ts +++ b/src/core.ts @@ -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'; diff --git a/src/use-provide-context.ts b/src/use-provide-context.ts new file mode 100644 index 0000000..58ce965 --- /dev/null +++ b/src/use-provide-context.ts @@ -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 extends Hook<[Context, T, unknown[]], void, Element> { + listeners: Set<(value: T) => void>; + + constructor( + id: number, + state: State, + private context: Context, + 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>): 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, 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); + } + } +); diff --git a/test/context.test.ts b/test/context.test.ts index 024fc3c..2ced322 100644 --- a/test/context.test.ts +++ b/test/context.test.ts @@ -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() { @@ -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`
@@ -81,6 +90,12 @@ describe('context', function() {
+ +
+ + + +
`; } @@ -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']); From 23b754151901874c046cee3e181519b352f57dbe Mon Sep 17 00:00:00 2001 From: Cristian Necula Date: Fri, 4 Nov 2022 14:08:25 +0200 Subject: [PATCH 2/2] docs: add documentation for useProvideContext --- docs/docs/hooks/index.md | 1 + docs/docs/hooks/useProvideContext.md | 63 ++++++++++++++++++++++++++++ docs/docs/index.md | 1 + readme.md | 1 + 4 files changed, 66 insertions(+) create mode 100644 docs/docs/hooks/useProvideContext.md diff --git a/docs/docs/hooks/index.md b/docs/docs/hooks/index.md index 73f3ef3..a398cca 100644 --- a/docs/docs/hooks/index.md +++ b/docs/docs/hooks/index.md @@ -10,6 +10,7 @@ Currently Haunted supports the following hooks: - [useEffect](./useEffect/) - [useLayoutEffect](./useLayoutEffect/) - [useMemo](./useMemo/) +- [useProvideContext](./useProvideContext/) - [useReducer](./useReducer/) - [useRef](./useRef/) - [useState](./useState/) diff --git a/docs/docs/hooks/useProvideContext.md b/docs/docs/hooks/useProvideContext.md new file mode 100644 index 0000000..6c3ee4e --- /dev/null +++ b/docs/docs/hooks/useProvideContext.md @@ -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` + + + + + + + html` +

${value}

+ `} + >
+
+ `; +} + +customElements.define("use-provide-context", component(App)); +``` + +```html playground-file use-provide-context index.html + + +``` + +## API diff --git a/docs/docs/index.md b/docs/docs/index.md index 73c388d..82ab94e 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -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/) diff --git a/readme.md b/readme.md index 8794ccb..95347eb 100644 --- a/readme.md +++ b/readme.md @@ -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/)