UNPKG

@zeix/ui-element

Version:

UIElement - a HTML-first library for reactive Web Components

148 lines (132 loc) 4.52 kB
import { type Cleanup, type MaybeSignal, type Signal, isFunction, toSignal, } from '@zeix/cause-effect' import { type Component, type ComponentProps } from '../component' /** @see https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md */ /* === Types === */ /** * A context key. * * A context key can be any type of object, including strings and symbols. The * Context type brands the key type with the `__context__` property that * carries the type of the value the context references. */ type Context<K, V> = K & { __context__: V } /** * An unknown context type */ type UnknownContext = Context<unknown, unknown> /** * A helper type which can extract a Context value type from a Context type */ type ContextType<T extends UnknownContext> = T extends Context<infer _, infer V> ? V : never /** * A callback which is provided by a context requester and is called with the value satisfying the request. * This callback can be called multiple times by context providers as the requested value is changed. */ type ContextCallback<V> = (value: V, unsubscribe?: () => void) => void declare global { interface HTMLElementEventMap { /** * A 'context-request' event can be emitted by any element which desires * a context value to be injected by an external provider. */ 'context-request': ContextRequestEvent<Context<unknown, unknown>> } } /* === Constants === */ const CONTEXT_REQUEST = 'context-request' /* === Exported class === */ /** * Class for context-request events * * An event fired by a context requester to signal it desires a named context. * * A provider should inspect the `context` property of the event to determine if it has a value that can * satisfy the request, calling the `callback` with the requested value if so. * * If the requested context event contains a truthy `subscribe` value, then a provider can call the callback * multiple times if the value is changed, if this is the case the provider should pass an `unsubscribe` * function to the callback which requesters can invoke to indicate they no longer wish to receive these updates. * * @class ContextRequestEvent * @extends {Event} * * @property {T} context - context key * @property {ContextCallback<ContextType<T>>} callback - callback function for value getter and unsubscribe function * @property {boolean} [subscribe=false] - whether to subscribe to context changes */ class ContextRequestEvent<T extends UnknownContext> extends Event { constructor( public readonly context: T, public readonly callback: ContextCallback<ContextType<T>>, public readonly subscribe: boolean = false, ) { super(CONTEXT_REQUEST, { bubbles: true, composed: true, }) } } /** * Provide a context for descendant component consumers * * @since 0.13.3 * @param {Context<K, Signal<P[K]>>[]} contexts - array of contexts to provide * @returns {(host: Component<P>) => Cleanup} - function to add an event listener for ContextRequestEvent returning a cleanup function to remove the event listener */ const provideContexts = <P extends ComponentProps, K extends keyof P>( contexts: Context<K, Signal<P[K]>>[], ): ((host: Component<P>) => Cleanup) => (host: Component<P>) => { const listener = (e: ContextRequestEvent<UnknownContext>) => { const { context, callback } = e if ( contexts.includes(context as Context<K, Signal<P[K]>>) && isFunction(callback) ) { e.stopImmediatePropagation() callback(host.getSignal(String(context))) } } host.addEventListener(CONTEXT_REQUEST, listener) return () => host.removeEventListener(CONTEXT_REQUEST, listener) } /** * Consume a context value for a component. * * @since 0.13.1 * @param {Context<K, Signal<P[K]>>} context - context key to consume * @param {MaybeSignal<P[K]>} fallback - fallback value to use if context is not provided * @returns {(host: C) => Signal<T>} - a function that returns the consumed context signal or a signal of the fallback value */ const fromContext = <T extends {}, C extends HTMLElement>( context: Context<string, Signal<T>>, fallback: MaybeSignal<T>, ): ((host: C) => Signal<T>) => (host: C) => { let consumed: Signal<T> = toSignal(fallback) host.dispatchEvent( new ContextRequestEvent(context, (value: Signal<T>) => { consumed = value }), ) return consumed } export { type Context, type UnknownContext, type ContextType, CONTEXT_REQUEST, ContextRequestEvent, provideContexts, fromContext, }