UNPKG

@symbiotejs/symbiote

Version:

Symbiote.js - close-to-platform frontend library for building super-powered web components

188 lines (169 loc) 4.74 kB
import PubSub from './PubSub.js'; export class AppRouter { /** * @typedef {{title?: String, default?: Boolean, error?: Boolean}} RouteDescriptor */ /** @type {() => void} */ static #onPopstate; /** @type {String} */ static #separator; /** @type {String} */ static #routingEventName; /** @type {Object<string, RouteDescriptor>} */ static appMap = Object.create(null); static #print(msg) { console.warn(msg); } /** @param {String} title */ static setDefaultTitle(title) { this.defaultTitle = title; } /** @param {Object<string, {}>} map */ static setRoutingMap(map) { Object.assign(this.appMap, map); for (let route in this.appMap) { if (!this.defaultRoute && this.appMap[route].default === true) { this.defaultRoute = route; } else if (!this.errorRoute && this.appMap[route].error === true) { this.errorRoute = route; } } } /** @param {String} name */ static set routingEventName(name) { /** @private */ this.#routingEventName = name; } /** @returns {String} */ static get routingEventName() { return this.#routingEventName || 'sym-on-route'; } static readAddressBar() { let result = { route: null, options: {}, }; let paramsArr = window.location.search.split(this.separator); paramsArr.forEach((part) => { if (part.includes('?')) { result.route = part.replace('?', ''); } else if (part.includes('=')) { let pair = part.split('='); result.options[pair[0]] = decodeURI(pair[1]); } else { result.options[part] = true; } }); return result; } static notify() { let routeBase = this.readAddressBar(); let routeScheme = this.appMap[routeBase.route]; if (routeScheme && routeScheme.title) { document.title = routeScheme.title; } if (routeBase.route === null && this.defaultRoute) { this.applyRoute(this.defaultRoute); return; } else if (!routeScheme && this.errorRoute) { this.applyRoute(this.errorRoute); return; } else if (!routeScheme && this.defaultRoute) { this.applyRoute(this.defaultRoute); return; } else if (!routeScheme) { this.#print(`Route "${routeBase.route}" not found...`); return; } let event = new CustomEvent(AppRouter.routingEventName, { detail: { route: routeBase.route, options: {...(routeScheme || {}), ...routeBase.options}, }, }); window.dispatchEvent(event); } /** * @param {String} route * @param {Object<string, any>} [options] */ static reflect(route, options = {}) { let routeScheme = this.appMap[route]; if (!routeScheme) { this.#print('Wrong route: ' + route); return; } let routeStr = '?' + route; for (let prop in options) { if (options[prop] === true) { routeStr += this.separator + prop; } else { routeStr += this.separator + prop + '=' + `${options[prop]}`; } } let title = routeScheme.title || this.defaultTitle || ''; try { window.history.pushState(null, title, routeStr); } catch (err) { console.warn('AppRouter: History API is not available.'); } document.title = title; } /** * @param {String} route * @param {Object<string, any>} [options] */ static applyRoute(route, options = {}) { this.reflect(route, options); this.notify(); } /** @param {String} char */ static setSeparator(char) { /** @private */ this.#separator = char; } /** @returns {String} */ static get separator() { return this.#separator || '&'; } /** * @param {String} ctxName * @param {Object<string, RouteDescriptor>} routingMap * @returns {PubSub} */ static initRoutingCtx(ctxName, routingMap) { this.setRoutingMap(routingMap); let routingCtx = PubSub.registerCtx( { route: null, options: null, title: null, }, ctxName ); window.addEventListener(this.routingEventName, (/** @type {CustomEvent} */ e) => { routingCtx.multiPub({ options: e.detail.options, title: e.detail.options?.title || this.defaultTitle || '', route: e.detail.route, }); }); AppRouter.notify(); this.#initPopstateListener(); return routingCtx; } static #initPopstateListener() { if (this.#onPopstate) { return; } this.#onPopstate = () => { this.notify(); }; window.addEventListener('popstate', this.#onPopstate); } static removePopstateListener() { window.removeEventListener('popstate', this.#onPopstate); this.#onPopstate = null; } } export default AppRouter;