/** * Service for navigating within the application * * The {@link Navigator} enables programmatic navigation to different locations * (routes) within the Lime platform. * * Navigation can include: * - Path changes (routing to different components) * - Query parameter updates * - Hash fragment changes * - Custom state attached to history entries * * All navigation triggers a {@link NavigationEvent} via {@link EventDispatcher}, * allowing other components to respond to location changes. * * Routes must be registered with {@link RouteRegistry} before navigation. * * @example Basic navigation * ```ts * import { PlatformServiceName } from '@limetech/lime-web-components'; * * const navigator = platform.get(PlatformServiceName.Navigator); * * navigator.navigate('/settings'); * ``` * * @example Navigation with query parameters * ```ts * import { PlatformServiceName } from '@limetech/lime-web-components'; * * const navigator = platform.get(PlatformServiceName.Navigator); * * // Results in /users?page=2&filter=active * navigator.navigate('/users', { page: 2, filter: 'active' }); * ``` * * @example Advanced navigation with all options * ```ts * import { PlatformServiceName } from '@limetech/lime-web-components'; * * const navigator = platform.get(PlatformServiceName.Navigator); * * navigator.navigate({ * path: '/products/123', * query: { tab: 'reviews' }, * hash: '#review-456', * state: { fromSearch: true }, * method: 'push', * }); * ``` * * @example Listening to navigation events * ```ts * import { * NavigationEvent, * PlatformServiceName, * } from '@limetech/lime-web-components'; * * const eventDispatcher = platform.get(PlatformServiceName.EventDispatcher); * * const onNavigate = (event: NavigationEvent) => { * console.log('Navigated to', event.detail.path); * console.log('Query params', event.detail.query); * console.log('State', event.detail.state); * }; * * eventDispatcher.addListener('navigate', onNavigate); * * // Remember to remove the listener in `disconnectedCallback()` * eventDispatcher.removeListener('navigate', onNavigate); * ``` * * @public * @group Navigation */ export interface Navigator { /** * Navigate to a new path with optional query parameters * * This is a convenience overload for simple navigation scenarios. For more * control over navigation (including hash, state, or method), use the * {@link LocationChange} overload. * * @emits navigate - Triggers a {@link NavigationEvent} via {@link EventDispatcher} * * @param path - The path to navigate to (e.g., '/users/123') * @param query - Optional query parameters to append to the URL * * @example Simple path navigation * ```ts * import { PlatformServiceName } from '@limetech/lime-web-components'; * * const navigator = platform.get(PlatformServiceName.Navigator); * * navigator.navigate('/settings'); * ``` * * @example Navigation with query parameters * ```ts * import { PlatformServiceName } from '@limetech/lime-web-components'; * * const navigator = platform.get(PlatformServiceName.Navigator); * * // Results in /users?page=2&filter=active * navigator.navigate('/users', { page: 2, filter: 'active' }); * ``` */ navigate(path: string, query?: Location['query']): void; /** * Navigate to a new location or update the current location * * This overload provides full control over all aspects of navigation, * including path, query parameters, hash, state, and history management. * * ## History Management * * By default, the navigation method is automatically determined: * - If the path is unchanged (only query/hash/state changes), the current * history entry is **replaced** * - If the path changes, a new history entry is **pushed** * * Override this behavior by setting `location.method` to 'push' or 'replace'. * * ## State Handling * * - When **pushing**, `state` defaults to `null` if not provided * - When **replacing**, the existing state is preserved unless a new value is provided * * @emits navigate - Triggers a {@link NavigationEvent} via {@link EventDispatcher} * * @param location - Location data to navigate to or update * * @example Update only query parameters (replaces current entry) * ```ts * import { PlatformServiceName } from '@limetech/lime-web-components'; * * const navigator = platform.get(PlatformServiceName.Navigator); * * navigator.navigate({ query: { page: 3 } }); * ``` * * @example Navigate with custom state * ```ts * import { PlatformServiceName } from '@limetech/lime-web-components'; * * const navigator = platform.get(PlatformServiceName.Navigator); * * navigator.navigate({ * path: '/products/123', * state: { fromSearch: true, previousPath: '/search' }, * }); * ``` * * @example Override the automatic push or replace choice * ```ts * import { PlatformServiceName } from '@limetech/lime-web-components'; * * const navigator = platform.get(PlatformServiceName.Navigator); * * // Replace even though the path changes * navigator.navigate({ * path: '/products/456', * method: 'replace', * }); * * // Push even though the path is unchanged * navigator.navigate({ * query: { page: 2 }, * method: 'push', * }); * ``` * * @example Navigate with hash and query parameters * ```ts * import { PlatformServiceName } from '@limetech/lime-web-components'; * * const navigator = platform.get(PlatformServiceName.Navigator); * * // Results in /docs/api?version=latest#parameters * navigator.navigate({ * path: '/docs/api', * query: { version: 'latest' }, * hash: '#parameters', * }); * ``` */ navigate(location: LocationChange): void; /** * Get the current location information * * Returns the complete current location including path, query parameters, * hash, and state. * * @returns Current location object * * @example Read the current location * ```ts * import { PlatformServiceName } from '@limetech/lime-web-components'; * * const navigator = platform.get(PlatformServiceName.Navigator); * const location = navigator.getLocation(); * * console.log('Current path', location.path); * console.log('Query params', location.query); * console.log('Hash', location.hash); * console.log('State', location.state); * ``` */ getLocation(): Location; /** * Add a navigation blocker to prevent unwanted navigation * * Blockers are useful for preventing users from accidentally navigating * away when there is unsaved data or ongoing work that would be lost. * * The blocker function is called before navigation occurs. Return `true` * to block navigation, or `false` to allow it. If blocked, use * {@link Transition.retry} to resume navigation later (e.g., after user * confirmation). * * @note Blockers are not guaranteed to be executed upon a navigation event, * for example a hard page unload cannot be intercepted this way. When they * are executed, blockers run in the order they were added until one * returns `true` or all have been called. * * @param blocker - Function to evaluate whether navigation should be blocked * * @example Basic unsaved changes blocker * ```ts * import { * Blocker, * PlatformServiceName, * Transition, * } from '@limetech/lime-web-components'; * * const navigator = platform.get(PlatformServiceName.Navigator); * * let hasUnsavedChanges = false; * * const blocker: Blocker = (transition: Transition) => { * if (!hasUnsavedChanges) { * return false; * } * * if (confirm('You have unsaved changes. Leave anyway?')) { * hasUnsavedChanges = false; * navigator.removeBlocker(blocker); * transition.retry(); * } * * return true; * }; * * navigator.addBlocker(blocker); * * // Remember to remove the blocker in `disconnectedCallback()` * navigator.removeBlocker(blocker); * ``` * * @example Asynchronous confirmation dialog * ```ts * import { * Blocker, * PlatformServiceName, * Transition, * } from '@limetech/lime-web-components'; * * const navigator = platform.get(PlatformServiceName.Navigator); * * let hasUnsavedChanges = false; * * const blocker: Blocker = (transition: Transition) => { * if (!hasUnsavedChanges) { * return false; * } * * void confirmNavigation(transition); * * // Block for now, `retry()` resumes the navigation if the user confirms * return true; * }; * * async function confirmNavigation(transition: Transition) { * const shouldNavigate = await showConfirmDialog(); * * if (!shouldNavigate) { * return; * } * * hasUnsavedChanges = false; * navigator.removeBlocker(blocker); * transition.retry(); * } * * // Stands in for the dialog the component would open * async function showConfirmDialog(): Promise { * return confirm('You have unsaved changes. Leave anyway?'); * } * * navigator.addBlocker(blocker); * ``` */ addBlocker(blocker: Blocker): void; /** * Remove a previously registered navigation blocker * * Always remove blockers in `disconnectedCallback()` to prevent memory * leaks and unintended blocking behavior. * * The blocker reference must be the exact same function instance that * was passed to {@link addBlocker}. * * @param blocker - The blocker function to remove * * @example Remove a blocker * ```ts * import { Blocker, PlatformServiceName } from '@limetech/lime-web-components'; * * const navigator = platform.get(PlatformServiceName.Navigator); * * const blocker: Blocker = () => false; * * navigator.addBlocker(blocker); * * // The same function instance has to be passed back in * navigator.removeBlocker(blocker); * ``` */ removeBlocker(blocker: Blocker): void; /** * Create a URL object from location data * * Converts location information (path, query, hash) into a full URL object. * This is useful for generating links or inspecting what the final URL * will be without actually navigating. * * @note The `state` property is ignored, as state is not part of the URL. * * @param location - Location data to convert to a URL * @returns URL object representing the location * * @example Inspect the parts of a generated URL * ```ts * import { PlatformServiceName } from '@limetech/lime-web-components'; * * const navigator = platform.get(PlatformServiceName.Navigator); * * const url = navigator.createUrl({ * path: '/products/123', * query: { tab: 'reviews' }, * hash: '#review-456', * }); * * // https://example.com/products/123?tab=reviews#review-456 * console.log(url.toString()); * * console.log(url.pathname); // /products/123 * console.log(url.search); // ?tab=reviews * console.log(url.hash); // #review-456 * ``` * * @example Generate a link element * ```tsx * import { h } from '@stencil/core'; * import { PlatformServiceName } from '@limetech/lime-web-components'; * * class ProductLink { * public render() { * const navigator = platform.get(PlatformServiceName.Navigator); * const url = navigator.createUrl({ * path: '/products/123', * query: { source: 'list' }, * }); * * return View Product; * } * } * ``` */ createUrl(location: Partial): URL; } /** * Represents a complete location in the application * * A location consists of a path, query parameters, hash fragment, and * optional state data. This type is used by {@link Navigator} to represent * both the current location and navigation targets. * * @example Describe a complete location * ```ts * import { Location, PlatformServiceName } from '@limetech/lime-web-components'; * * const target: Location = { * path: '/products/123', * query: { tab: 'reviews', sort: 'date' }, * hash: '#review-456', * state: { fromSearch: true }, * }; * * const navigator = platform.get(PlatformServiceName.Navigator); * navigator.navigate(target); * ``` * * @public * @group Navigation */ export type Location = { /** * The URL path * * The path portion of the URL, not including query parameters or hash. */ path: string; /** * Parsed URL query parameters * * Query string parameters parsed as an object. Values are automatically * parsed using `JSON.parse()` when possible, allowing for typed values. */ query: Record; /** * URL fragment identifier (hash) * * The hash portion of the URL, including the leading `#` character. */ hash: string; /** * Navigation state attached to this location * * Custom state data stored with this history entry. State is not visible * in the URL and is preserved when navigating back/forward through history. * * Useful for passing context between routes without exposing it in the URL. */ state: unknown; }; /** * Partial location data for navigation * * Used with {@link Navigator.navigate} to specify which parts of the location * to change. All properties are optional, allowing selective updates. * * @example Change part of the location * ```ts * import { * LocationChange, * PlatformServiceName, * } from '@limetech/lime-web-components'; * * const navigator = platform.get(PlatformServiceName.Navigator); * * // Update only the query parameters * const queryOnly: LocationChange = { * query: { page: 2 }, * }; * navigator.navigate(queryOnly); * * // Navigate with a path and state * const withState: LocationChange = { * path: '/products/123', * state: { fromList: true }, * method: 'push', * }; * navigator.navigate(withState); * ``` * * @public * @group Navigation */ export type LocationChange = Partial & { /** * History navigation method * * Controls whether navigation creates a new history entry or replaces * the current one: * - `'push'`: Add a new entry to the history stack (default for path changes) * - `'replace'`: Replace the current entry (default for query/hash/state changes) * * If not specified, the method is automatically determined based on whether * the path changed. * * @example Override the automatic push or replace choice * ```ts * import { PlatformServiceName } from '@limetech/lime-web-components'; * * const navigator = platform.get(PlatformServiceName.Navigator); * * // Replace even though the path changes * navigator.navigate({ * path: '/products/456', * method: 'replace', * }); * * // Push even though the path is unchanged * navigator.navigate({ * query: { page: 2 }, * method: 'push', * }); * ``` */ method?: 'push' | 'replace'; }; /** * Event emitted when navigation occurs * * Dispatched via {@link EventDispatcher} whenever navigation happens, whether * initiated programmatically or by user actions (back/forward buttons). * * The event detail contains the complete {@link Location} that was navigated to. * * @event navigate * * @example Listen for navigation * ```ts * import { * NavigationEvent, * PlatformServiceName, * } from '@limetech/lime-web-components'; * * const eventDispatcher = platform.get(PlatformServiceName.EventDispatcher); * * const onNavigate = (event: NavigationEvent) => { * console.log('Navigated to', event.detail.path); * console.log('Query params', event.detail.query); * console.log('State', event.detail.state); * }; * * eventDispatcher.addListener('navigate', onNavigate); * * // Remember to remove the listener in `disconnectedCallback()` * eventDispatcher.removeListener('navigate', onNavigate); * ``` * * @public * @group Navigation */ export type NavigationEvent = CustomEvent; /** * Function to retry blocked navigation * * Provided by {@link Transition} to resume navigation after it was blocked * by a {@link Blocker}. Call this after user confirmation or other conditions * are met. * * @example Resume navigation after confirmation * ```ts * import { * Blocker, * PlatformServiceName, * Transition, * } from '@limetech/lime-web-components'; * * const navigator = platform.get(PlatformServiceName.Navigator); * * let hasUnsavedChanges = false; * * const blocker: Blocker = (transition: Transition) => { * if (!hasUnsavedChanges) { * return false; * } * * void confirmNavigation(transition); * * // Block for now, `retry()` resumes the navigation if the user confirms * return true; * }; * * async function confirmNavigation(transition: Transition) { * const shouldNavigate = await showConfirmDialog(); * * if (!shouldNavigate) { * return; * } * * hasUnsavedChanges = false; * navigator.removeBlocker(blocker); * transition.retry(); * } * * // Stands in for the dialog the component would open * async function showConfirmDialog(): Promise { * return confirm('You have unsaved changes. Leave anyway?'); * } * * navigator.addBlocker(blocker); * ``` * * @public * @group Navigation */ export type Retry = () => void; /** * Navigation transition information * * Describes a pending navigation from one location to another. Provided to * {@link Blocker} functions to allow inspection of the navigation and optional * retry after blocking. * * @example Inspect a pending navigation * ```ts * import { * Blocker, * PlatformServiceName, * Transition, * } from '@limetech/lime-web-components'; * * const navigator = platform.get(PlatformServiceName.Navigator); * * const blocker: Blocker = (transition: Transition) => { * console.log('Navigating from', transition.from.path); * console.log('Navigating to', transition.to.path); * * return transition.to.path.startsWith('/admin'); * }; * * navigator.addBlocker(blocker); * ``` * * @public * @group Navigation */ export type Transition = { /** * Target location for navigation * * The {@link Location} that will be navigated to if not blocked. */ to: Location; /** * Current location before navigation * * The {@link Location} before the navigation occurs. */ from: Location; /** * Function to retry the blocked navigation * * Call this function to resume navigation after it was blocked. Typically * called after user confirmation or other async conditions are met. * * @example Resume navigation after confirmation * ```ts * import { * Blocker, * PlatformServiceName, * Transition, * } from '@limetech/lime-web-components'; * * const navigator = platform.get(PlatformServiceName.Navigator); * * let hasUnsavedChanges = false; * * const blocker: Blocker = (transition: Transition) => { * if (!hasUnsavedChanges) { * return false; * } * * void confirmNavigation(transition); * * // Block for now, `retry()` resumes the navigation if the user confirms * return true; * }; * * async function confirmNavigation(transition: Transition) { * const shouldNavigate = await showConfirmDialog(); * * if (!shouldNavigate) { * return; * } * * hasUnsavedChanges = false; * navigator.removeBlocker(blocker); * transition.retry(); * } * * // Stands in for the dialog the component would open * async function showConfirmDialog(): Promise { * return confirm('You have unsaved changes. Leave anyway?'); * } * * navigator.addBlocker(blocker); * ``` */ retry: Retry; }; /** * Function to determine if navigation should be blocked * * A blocker is registered with {@link Navigator.addBlocker} to prevent * navigation under certain conditions (e.g., unsaved changes). * * Return `true` to block navigation, or `false` to allow it. If blocked, * use {@link Transition.retry} to resume navigation later. * * @param transition - Information about the pending navigation * @returns `true` to block navigation, `false` to allow it * * @example Block navigation when there are unsaved changes * ```ts * import { * Blocker, * PlatformServiceName, * Transition, * } from '@limetech/lime-web-components'; * * const navigator = platform.get(PlatformServiceName.Navigator); * * let hasUnsavedChanges = false; * * const blocker: Blocker = (transition: Transition) => { * if (!hasUnsavedChanges) { * return false; * } * * if (confirm('You have unsaved changes. Leave anyway?')) { * hasUnsavedChanges = false; * navigator.removeBlocker(blocker); * transition.retry(); * } * * return true; * }; * * navigator.addBlocker(blocker); * * // Remember to remove the blocker in `disconnectedCallback()` * navigator.removeBlocker(blocker); * ``` * * @public * @group Navigation */ export type Blocker = (transition: Transition) => boolean;