import { ComponentDescriptor, LimeWebComponent } from '../core'; /** * Service for registering and routing to web components * * The {@link RouteRegistry} enables URL-based routing to web components within * the Lime platform. * * Register URL patterns mapped to component tag names, then navigate to those * URLs using {@link Navigator}. The routing system will instantiate the * component and pass URL parameters as props. * * A parameter such as `/users/:userId` is extracted and passed as a prop. A * value that parses as a number arrives as a `number`, anything else stays a * `string`. * * A `limetype` parameter builds a {@link LimeWebComponentContext} for the * component. An `id` in the same path joins that context, and both are then * removed from the props. An `id` without a `limetype` creates no context and * stays an ordinary prop. * * ## Reserved Property Names * * The following property names are reserved by {@link RouteComponent} and * cannot be used as path parameters: * - `platform` - The platform instance * - `context` - The component context * - `query` - URL query parameters * - `hash` - URL hash fragment * - `state` - Navigation state * - `onTitleChanged` - Handler bound to the {@link RouteComponent.titleChanged} event * * @example Registering a route and navigating to it * ```ts * import { PlatformServiceName } from '@limetech/lime-web-components'; * * const routeRegistry = platform.get(PlatformServiceName.RouteRegistry); * const navigator = platform.get(PlatformServiceName.Navigator); * * routeRegistry.registerRoute('/users/:userId', 'user-profile'); * * // Renders * navigator.navigate('/users/123'); * ``` * * @public * @group Registering routes */ export interface RouteRegistry { /** * Register a component for a URL pattern * * Maps a URL path pattern to a web component tag name. When navigation * occurs to a matching path, the component will be instantiated and * rendered with URL parameters passed as props. * * The path pattern uses the URLPattern API syntax. Common patterns: * - `/static` - Exact match * - `/users/:id` - Named parameter * - `/files/:path*` - Wildcard parameter * - `/admin/:section(users|roles)` - Enum parameter * * @param pathPattern - URL path pattern using URLPattern syntax. * See [MDN URLPattern docs](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API) * for pattern syntax details. * @param component - Tag name of the web component to render for this route * * @example Registering each kind of pattern * ```ts * import { PlatformServiceName } from '@limetech/lime-web-components'; * * const routeRegistry = platform.get(PlatformServiceName.RouteRegistry); * * // Exact match * routeRegistry.registerRoute('/dashboard', 'app-dashboard'); * * // Named parameters, passed to the component as props * routeRegistry.registerRoute('/users/:userId', 'user-profile'); * routeRegistry.registerRoute( * '/posts/:postId/comments/:commentId', * 'comment-view' * ); * * // Wildcard parameter * routeRegistry.registerRoute('/files/:path*', 'file-browser'); * * // Enum parameter * routeRegistry.registerRoute('/admin/:section(users|roles)', 'admin-panel'); * * // `limetype` and `id` together give the component a context * routeRegistry.registerRoute('/object/:limetype/:id', 'object-viewer'); * ``` */ registerRoute(pathPattern: string, component: string): void; /** * Find the component registered for a given path * * Matches the path against all registered route patterns and returns the * component name and extracted parameters if a match is found. * * This is typically used internally by the platform routing system, but * can be useful for testing or custom routing logic. * * @param path - The URL path to match (e.g., '/users/123') * @returns Matched component name and parameters, or `undefined` if no route matches * * @example Matching a path * ```ts * import { PlatformServiceName } from '@limetech/lime-web-components'; * * const routeRegistry = platform.get(PlatformServiceName.RouteRegistry); * routeRegistry.registerRoute('/users/:userId', 'user-profile'); * * const match = routeRegistry.findComponent('/users/123'); * * // `undefined` when no registered pattern matches the path * const componentName = match?.name; * * // 123, not '123' * const userId = match?.props['userId']; * ``` */ findComponent(path: string): MatchedComponent | undefined; } /** * Type of a single path parameter after a route has matched * * @see {@link RouteRegistry} for how a path is turned into parameters * * @public * @group Registering routes */ export type RouteParam = string | number | undefined; /** * Result from matching a path to a registered route * * Contains the component name and extracted path parameters from the URL. * Returned by {@link RouteRegistry.findComponent}. The fields are described * on {@link ComponentDescriptor}, which this narrows so that a match always * carries `props`, even when the path had no parameters. * * Only `props` is narrowed, on purpose. Widening this to every field of * {@link ComponentDescriptor} would make each field added there required * here too, which is a contract the router never agreed to. * * @example A matched route * ```ts * import type { MatchedComponent } from '@limetech/lime-web-components'; * * // Registered as '/users/:userId/posts/:postId' and matched against * // '/users/123/posts/456' * const match: MatchedComponent = { * name: 'post-viewer', * props: { userId: 123, postId: 456 }, * }; * ``` * * @public * @group Registering routes */ export type MatchedComponent = ComponentDescriptor & Required, 'props'>>; /** * Interface for components that can be routed to * * Components registered with {@link RouteRegistry} should implement this * interface to receive routing information such as query parameters, hash * fragments, and navigation state. * * This extends {@link LimeWebComponent}, so all routed components also * receive the standard `platform` and `context` props. * * @example A routed component * ```ts * import { Event, Prop, type EventEmitter } from '@stencil/core'; * import type { * LimeWebComponentContext, * LimeWebComponentPlatform, * RouteComponent, * } from '@limetech/lime-web-components'; * * type SearchOrigin = { * fromSearch: boolean; * searchTerm: string; * }; * * // Registered for '/products/:productId' and visited as * // /products/123?category=electronics&inStock=true#reviews * class ProductDetails implements RouteComponent { * @Prop() * public platform: LimeWebComponentPlatform; * * @Prop() * public context: LimeWebComponentContext; * * @Prop() * public query: Record; * * @Prop() * public hash: string; * * @Prop() * public state: unknown; * * @Event() * public titleChanged: EventEmitter; * * public componentDidLoad() { * // 'electronics' * const category = this.query['category']; * * // true, not 'true' * const inStock = this.query['inStock']; * * // '#reviews' -> 'reviews' * const section = this.hash.slice(1); * document.querySelector(`#${section}`)?.scrollIntoView(); * * // Whatever the caller passed to Navigator.navigate * const origin = this.state as SearchOrigin | undefined; * * if (origin?.fromSearch) { * const term = origin.searchTerm; * } * * this.titleChanged.emit('Product Details'); * } * } * ``` * * @public * @group Registering routes */ export interface RouteComponent extends LimeWebComponent { /** * URL query parameters from the current location * * Parsed from the URL query string. Values are automatically parsed * using `JSON.parse()` when possible. */ query?: Record; /** * URL fragment identifier from the current location * * The hash portion of the URL, including the leading `#` character. */ hash?: string; /** * Navigation state from the current location * * Custom state data passed during navigation via {@link Navigator.navigate}. * This state is stored in the browser's history entry. */ state?: unknown; /** * Event emitter for changing the document title * * Emit this event to update the browser's document title. Emit the name of * the page on its own, the host decides how to join it with the name of * the application. */ titleChanged?: { emit: (data?: string | undefined) => CustomEvent; }; }