import { ContextMap } from './context-map'; import { ContextMetadata } from './metadata'; import { ContextScope, ContextScopeOptions } from './scope'; /** * Handle returned by {@link ContextRegistry.provide} for the lifetime of * a provider registration. * * @beta * @group Context */ export interface ContextProviderHandle { /** * Detach the provider from its host. Call from * `disconnectedCallback` (or equivalent) so the registry stops * walking through it. */ teardown(): void; /** * Signal that the provided value has changed. Triggers re-evaluation * in active subscribers whose resolved value could have changed. * * Reactive providers call `notify` whenever their underlying value * changes. Non-reactive providers ignore `notify` and only call * `teardown` on disconnect. */ notify(): void; } /** * Central place for ambient context: registration, DOM-walked provider * lookup, direct key lookup, reactive subscription, and scope * construction. * * This package only describes the contract; the actual behavior is * supplied by an implementation, reached through the platform service * `contextRegistry`. * * What the API does, by area: * * - **Registration.** {@link ContextRegistry.register | register} * declares a context key once at bootstrap and accepts an optional * session-wide fallback. {@link ContextRegistry.list | list} * enumerates registered keys. * - **Per-host providers.** {@link ContextRegistry.provide | provide} * attaches a value source to a host element. Two copies of the same * component in different parts of the page each get their own value * without interfering. * - **Pull (snapshot reads).** {@link ContextRegistry.get | get} * returns the resolved value once. Used by rule evaluation and any * caller that wants a value on demand. * - **Push (reactive subscription).** {@link ContextRegistry.subscribe * | subscribe} delivers the current value immediately and again on * every change. Used by reactive consumers that drive component * re-renders without polling. * - **Scope construction.** {@link ContextRegistry.scope | scope} binds * a host plus optional overrides into a {@link ContextScope} that * answers `get(key)` repeatedly. Used by rule evaluation, command * guards, filter predicates, and anything else that wants a * pre-bound view of {@link ContextMap}. * * Each key resolves in order from most specific to least: the closest * DOM ancestor that registered a value through * {@link ContextRegistry.provide | provide} wins first, then a * session-wide fallback registered through * {@link ContextRegistry.register | register}. If neither answers, the * resolved value is `undefined`. * * @example Snapshot read * ```ts * import { Element } from '@stencil/core'; * import { * createLogger, * PlatformServiceName, * } from '@limetech/lime-web-components'; * * const logger = createLogger('my-component'); * * class MyComponent { * @Element() * private host: HTMLElement; * * public connectedCallback() { * const registry = platform.get(PlatformServiceName.ContextRegistry); * const user = registry.get('user', this.host); * * logger.debug('Resolved the current user', { * username: user?.username, * }); * } * } * ``` * * @example Reactive subscription * ```ts * import { Element, State } from '@stencil/core'; * import { PlatformServiceName, User } from '@limetech/lime-web-components'; * * class MyComponent { * @Element() * private host: HTMLElement; * * @State() * private user: User | undefined; * * private stop: () => void; * * public connectedCallback() { * const registry = platform.get(PlatformServiceName.ContextRegistry); * * this.stop = registry.subscribe('user', this.host, (user) => { * this.user = user; * }); * } * * public disconnectedCallback() { * this.stop(); * } * } * ``` * * @example Building a scope * ```ts * import { Element } from '@stencil/core'; * import { * createLogger, * LimeObjectDraft, * PlatformServiceName, * } from '@limetech/lime-web-components'; * * const logger = createLogger('my-component'); * * class MyFormComponent { * @Element() * private host: HTMLElement; * * // The persisted row overlaid with the edits the user has not saved yet. * private draft: LimeObjectDraft; * * public connectedCallback() { * const registry = platform.get(PlatformServiceName.ContextRegistry); * * // `host` resolves keys from the DOM, `with` pins fixed values on top. * const scope = registry.scope({ * host: this.host, * with: { limeobjectDraft: this.draft }, * }); * * logger.debug('Scope built', { * username: scope.get('user')?.username, * draftName: scope.get('limeobjectDraft')?.getValue('name'), * }); * } * } * ``` * * @beta * @group Context */ export interface ContextRegistry { /** * Declare that a context key exists. Called once per key at * bootstrap. * * The optional `fallback` is a session-wide value source, used when * no DOM ancestor has supplied a value. Good for keys that are the * same for the whole session, like the current user or device. * * Compare with {@link ContextRegistry.provide | provide}, which * supplies a value scoped to a specific host element. * * @typeParam K - The context id. Tying it to a known key makes the * fallback's return type match the key's value type. * @param metadata - Metadata for the key (id, label, description). * @param fallback - Optional session-wide value source. May return * `undefined` when no value is currently available; treated the * same as no fallback being registered. */ register(metadata: ContextMetadata, fallback?: () => ContextMap[K] | undefined): void; /** * Attach a value source for a context key to a host element. * Typically called during a component's lifetime. * * When something later resolves `key` against a host that is a * descendant of this element, the closest ancestor that registered * a provider wins. * * Reactive providers call `notify()` on the returned handle when * their underlying value changes. Non-reactive providers ignore * `notify` and only call `teardown()` on disconnect. * * @typeParam K - The context id. Pinning it to a known key makes * `getter`'s return type match the key's value type. * @param key - The context this provider answers. * @param host - Element to attach the provider to. * @param getter - Returns the current value. Called every time a * walk hits this provider, so keep it cheap. May return `undefined` * when no value is currently available; the walk then continues to * the next provider. * @returns A handle exposing `teardown` and `notify`. Call * `teardown` from `disconnectedCallback` (or equivalent) to detach * the provider. */ provide(key: K, host: HTMLElement, getter: () => ContextMap[K] | undefined): ContextProviderHandle; /** * Enumerate the registered context keys. */ list(): ContextMetadata[]; /** * Snapshot read of a context value. * * Resolves `key` against the DOM ancestors of `host` first, then * the session-wide fallback. Returns `undefined` when no provider * answers. * * @typeParam K - The context id. * @param key - The context to resolve. * @param host - Element to start the ancestor walk from. When * omitted, only the session-wide fallback is consulted. * @returns The resolved value, or `undefined` when no provider * answers. */ get(key: K, host?: HTMLElement): ContextMap[K] | undefined; /** * Subscribe to changes of a context value. * * The listener fires immediately with the current value and again * whenever the resolved value changes. The immediate fire is * synchronous, the listener is invoked before `subscribe` returns. * Returns a teardown function to stop the subscription. * * Reactive providers signal changes through * {@link ContextProviderHandle.notify | notify}. A subscription * also re-evaluates when providers are added or removed along the * ancestor chain. * * @typeParam K - The context id. * @param key - The context to observe. * @param host - Element to anchor the subscription to. Pass * `undefined` to only consult the session-wide fallback. * @param listener - Called immediately with the current value and * again on every change. * @returns Teardown function to stop the subscription. */ subscribe(key: K, host: HTMLElement | undefined, listener: (value: ContextMap[K] | undefined) => void): () => void; /** * Build a {@link ContextScope}, a host-bound typed view that * resolves {@link ContextMap} keys on demand. The only supported * way to construct one. * * - Pass `host` to walk the DOM from there. Typical for components. * - Pass `with` to hand in fixed values. Typical for tests and * headless code. * - Pass both to walk the DOM with overrides for specific context * keys. * - Pass nothing for an empty scope where every `get` returns * `undefined`. * * Rules are one consumer of `ContextScope` (a rule predicate reads * from one), but command guards, filter predicates, and anything * else that wants "bind a host plus overrides once, then read keys * repeatedly" builds a scope the same way. * * @param options - Optional construction options. * @returns A new scope. */ scope(options?: ContextScopeOptions): ContextScope; }