import * as plugins from './smartrouter.plugins.js'; import { QueryParams } from './smartrouter.classes.queryparams.js'; const routeLog = (message: string) => { console.log(`%c[Router]%c ${message}`, 'color: rgb(255, 105, 100);', 'color: inherit'); }; export interface IRouterOptions { debug?: boolean; } export type THandlerFunction = (routeArg: IRouteInfo) => Promise; export interface IRouteInfo { path: string; index: number; params: { [key: string]: string }; queryParams: { [key: string]: string }; } /** * Router */ export class SmartRouter { public options: IRouterOptions = { debug: false, }; public queryParams = new QueryParams(); /** * the routes we are handling */ public routes: Array<{ matchFunction: plugins.pathToRegExp.MatchFunction; handler: THandlerFunction; }> = []; /** * base path for this router */ private basePath: string; /** * Reference to the event listener function for cleanup */ private popstateListener: (event: PopStateEvent) => void; /** * Creates an instance of Router. */ constructor(optionsArg: IRouterOptions, basePath: string = '') { // lets set the router options this.options = { ...this.options, ...optionsArg, }; this.basePath = basePath; // lets subscribe to route changes this.popstateListener = (popStateEventArg) => { popStateEventArg.preventDefault(); this._handleRouteState(); }; window.addEventListener('popstate', this.popstateListener); } /** * Create a sub-router with a specific prefix * @param {string} subPath * @param {IRouterOptions} [options] */ public createSubRouter(subPath: string, options?: IRouterOptions): SmartRouter { const newBasePath = `${this.basePath}${subPath}`; return new SmartRouter({ ...this.options, ...options }, newBasePath); } /** * Push route state to history stack */ public async pushUrl(url: string = '/', state: any = {}) { const fullUrl = `${this.basePath}${url}`; if (fullUrl !== window.location.pathname) { window.history.pushState(state, window.document.title, fullUrl); } else { window.history.replaceState(state, window.document.title, fullUrl); } await this._handleRouteState(); } /** * Attach route with handler * @param {string|RegExp} routeArg * @param {function} handlerArg */ public on(routeArg: string, handlerArg: THandlerFunction) { const fullRoute = `${this.basePath}${routeArg}`; const routeObject = { matchFunction: plugins.pathToRegExp.match(fullRoute), handler: handlerArg, }; this.routes.push(routeObject); const removeFunction = () => { this.routes.splice(this.routes.indexOf(routeObject), 1); }; return removeFunction; } /** * Apply routes handler to current route */ async _handleRouteState() { const currentLocation = window.location.pathname; // lets find all wanted routes. const wantedRoutes = this.routes.filter((routeArg) => { return !!routeArg.matchFunction(currentLocation); }); for (const wantedRoute of wantedRoutes) { const routeResult = wantedRoute.matchFunction(currentLocation); wantedRoute.handler({ ...(routeResult.valueOf() as Object), queryParams: this.queryParams.getAllAsObject(), // TODO check wether entries is supported in typings } as IRouteInfo); // not waiting here } } /** * Destroy the router instance, removing all external references */ public destroy() { // Remove the event listener for popstate window.removeEventListener('popstate', this.popstateListener); // Clear all routes this.routes = []; } }