From 4069d4f6931cf6891f565f0e06b6ffedf43a6e5c Mon Sep 17 00:00:00 2001 From: Cristian Necula Date: Fri, 4 Nov 2022 03:36:29 +0200 Subject: [PATCH] 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']);