@zyrab/domo-router
Version:
History-based SPA router for Domo. Supports nested routes, navigation guards, and SSG-friendly behavior.
174 lines (159 loc) • 6.46 kB
JavaScript
// router.core.js
import { _root, setRoot, setRoutes, addListener, setPreviousUrl } from "./state.js";
import { saveScroll, restoreScroll, navigate, goBack } from "./history.js";
import { path, info, setInfo, getPreviousUrl, getBasePath } from "./utils.js";
import { load } from "./rendering.js";
/**
* Initializes the router by setting up event listeners for page load and history changes.
* It also initializes the root element where components will be rendered.
* @returns {void}
*/
function init() {
// Ensure we are in a browser environment before adding event listeners
if (typeof window !== "undefined") {
// Initialize _root element if it doesn't exist
if (!_root) {
const mainElement = document.body;
setRoot(mainElement);
// It's up to the user to append Router.mount() to their DOM
}
["DOMContentLoaded", "popstate"].forEach((event) =>
window.addEventListener(event, async () => {
saveScroll(getPreviousUrl()); // Save scroll of the page we're leaving
const currentUrl = path();
await load(currentUrl); // Load the new route
setPreviousUrl(currentUrl); // Update previous URL *after* loading
restoreScroll(); // Restore scroll for the new page
})
);
}
}
/**
* The main Router object, exposing all public API methods for navigation,
* route definition, and state access.
*/
const Router = {
/**
* Mount point of the router. Returns the root DOM element where components will be rendered.
* Users should append this element to their desired location in the DOM.
* @returns {HTMLElement} The root DOM element used by the router.
* @example
* document.body.appendChild(Router.mount());
*/
mount: () => {
// Ensure _root exists before returning it, initialize if not
if (!_root && typeof document !== "undefined") {
const mainElement = document.body;
setRoot(mainElement);
}
return _root;
},
/**
* Initializes the router by setting up event listeners and loading the initial route.
* This should be called once when your application starts.
* @returns {void}
* @example
* Router.init();
*/
init,
/**
* Programmatically navigates to a new route. This updates the browser's history.
* @param {string} targetPath - The path to navigate to (e.g., '/about', '/users/123').
* @returns {Promise<void>} A Promise that resolves once the new route is loaded and rendered.
* @example
* Router.goTo('/dashboard');
*/
goTo: (targetPath) => navigate(targetPath, load), // Use the general navigate function
/**
* Navigates one step back in the browser's history.
* @returns {void}
* @example
* Router.back();
*/
back: goBack,
/**
* Gets the current full path from the browser's location, including the hash.
* @returns {string} The current path (e.g., '/users/123#settings').
* @example
* console.log(Router.path()); // Outputs: /current/url
*/
path,
/**
* Gets the previous URL path the router navigated from.
* @returns {string} The previous URL path, or '/' if no previous navigation occurred.
* @example
* console.log(Router.prev()); // Outputs: /previous/page
*/
prev: getPreviousUrl,
/**
* Returns the base (first) segment of the current path.
* For `/blog/post-1`, it returns `/blog`. For `/`, it returns `/`.
* @returns {string} The base segment of the current path.
* @example
* // If current path is '/users/profile'
* console.log(Router.base()); // Outputs: /users
*/
base: getBasePath,
/**
* Returns comprehensive information about the currently active route.
* @returns {{ meta: object, params: object, segments: string[], base: string, path: string }}
* An object containing:
* - `meta`: Any metadata defined for the route (e.g., title, description).
* - `params`: An object of parameters extracted from dynamic route segments (e.g., `{ id: '123' }`).
* - `segments`: An array of URL path segments (e.g., `['/users', '/123']`).
* - `base`: The first segment of the path (e.g., '/users').
* - `path`: The full current path.
* @example
* // If route is '/user/:id' and current URL is '/user/456'
* const routeInfo = Router.info();
* console.log(routeInfo.params.id); // Outputs: 456
* console.log(routeInfo.meta.title); // Outputs: (from route config)
*/
info,
/**
* Manually sets the current route information. Primarily for internal use or advanced scenarios.
* @param {string} path - The path for which to set info.
* @param {object} [params={}] - Parameters for the path.
* @returns {void}
*/
setInfo, // Expose for specific use cases
/**
* Defines the routing structure for the application.
* This method should be called once to configure all your routes.
* Routes can be nested and include dynamic segments (e.g., `/:id`).
* A wildcard route (`'*'`) can be defined for 404/not found pages.
*
* @param {object} config - An object defining the route structure.
* Each key is a path segment, and its value is an object containing `component` (a function returning an HTMLElement or string)
* and optional `meta` data, or `children` for nested routes.
* @example
* Router.routes({
* '/': { component: () => Domo('h1').txt('Welcome').build(), meta: { title: "Home" } },
* '/products': {
* component: () => Domo('h2').txt('Products List').build(),
* children: {
* '/:productId': { component: (params) => Domo('div').txt(`Product ID: ${params.productId}`).build() }
* }
* },
* '*': { component: () => Domo('h1').txt('404 Not Found').build() }
* });
*/
routes: (config) => {
setRoutes(config);
},
/**
* Registers a listener callback function that will be invoked whenever the route changes.
* The listener receives the current route information as its argument.
* @param {(info: ReturnType<typeof info>) => void} fn - The callback function to register.
* @returns {void}
* @example
* Router.listen((routeInfo) => {
* console.log('Route changed to:', routeInfo.path);
* console.log('Params:', routeInfo.params);
* });
*/
listen: (fn) => {
if (typeof fn === "function") addListener(fn);
},
};
export default Router;