import { html, LitElement } from 'lit'; import type { TemplateResult } from 'lit'; import { property } from 'lit/decorators.js'; import { contextRequestEventName, isRouterContextRequest, requestRouter, } from './context.js'; import { UIRouterLit } from './core.js'; interface UiRouterContextEventDetail { uiRouter?: UIRouterLit; } /** * @internal */ export type UiRouterContextEvent = CustomEvent; /** The router never changes under a subscriber, so there is nothing to undo. */ const noUnsubscribe = () => {}; /** * @hideconstructor * @category components * * @slot - <ui-router> renders slotted content. * * @fires {CustomEvent} ui-router-context * * <ui-router> listens to the * ui-router-context event * and provides the uiRouter instance. * * It answers the community context-request protocol for * {@link routerContext} from the same listener position, so an element built * on @lit/context gets the router too. The two paths coexist and * neither sees the other's event: {@link UIRouterLitElement.seekRouter} asks * on ui-router-context first and falls back to * {@link requestRouter}. * * @summary * * This is the root ui-router component. * */ export class UIRouterLitElement extends LitElement { /** * Root uiRouter singleton. * If not provided, the element creates and assigns a new instance. */ @property({ attribute: false }) uiRouter: UIRouterLit | undefined; /** @internal */ static uiRouterContextEventName = 'ui-router-context'; /** @internal */ static uiRouterContextEvent(uiRouter?: UIRouterLit): UiRouterContextEvent { return new CustomEvent(this.uiRouterContextEventName, { bubbles: true, composed: true, detail: { uiRouter, }, }); } /** * Discovers the {@link UIRouterLit} instance provided by the nearest * enclosing <ui-router> element. * * Dispatches a bubbling, composed ui-router-context event from * the candidate element; the enclosing <ui-router> * answers it with its router instance. Returns undefined when * the candidate is not inside a <ui-router> (e.g. not * yet connected). * * This is the dependency-injection primitive for integrating external * reactivity systems (state stores, controllers) with the router context — * call it from hostConnected() / connectedCallback() * instead of prop-drilling the router instance. * * When no <ui-router> answers, it asks again with * {@link requestRouter}, so an ancestor providing {@link routerContext} * over the community protocol — a bare @lit/context * ContextProvider, say — is found as well. */ static seekRouter(candidate: Element): UIRouterLit | undefined { const uiRouterContextEvent = this.uiRouterContextEvent(); candidate.dispatchEvent(uiRouterContextEvent); return uiRouterContextEvent.detail.uiRouter ?? requestRouter(candidate); } /** @internal */ static onUiRouterContextEvent( uiRouter?: UIRouterLit, ): (event: UiRouterContextEvent) => void { return (event: UiRouterContextEvent) => { event.stopPropagation(); event.detail.uiRouter = uiRouter; }; } private readonly onUiRouterContextEvent = (event: UiRouterContextEvent) => { this.constructor.onUiRouterContextEvent(this.uiRouter)(event); }; /** * Answers a `context-request` for the router context. * * `subscribe` gets one call and a no-op unsubscribe: the element takes its * router on connect and does not swap it afterwards. */ private readonly onContextRequest = (event: Event) => { const { uiRouter } = this; if (!uiRouter || !isRouterContextRequest(event)) { return; } // stopped first: a throwing consumer must not leak the request outward event.stopImmediatePropagation(); event.callback(uiRouter, event.subscribe ? noUnsubscribe : undefined); }; /** @internal */ connectedCallback(): void { super.connectedCallback(); this.uiRouter = this.uiRouter || new UIRouterLit(); this.addEventListener( this.constructor.uiRouterContextEventName, this.onUiRouterContextEvent as EventListener, ); this.addEventListener(contextRequestEventName, this.onContextRequest); this.dispatchEvent(this.constructor.uiRouterContextEvent(this.uiRouter)); } /** @internal */ render(): TemplateResult { return html``; } } export interface UIRouterLitElement { /** @internal */ constructor: typeof UIRouterLitElement; }