Source

router/AdmRouter.js

"use strict"

/**
 *	----------------------------------------------------------------------------------------------
 *	Imports
 * 	----------------------------------------------------------------------------------------------
 */
import Route from 'route-parser';
import {eventBus, AdmEventBus} from '../event/AdmEventBus';
import {joinPath} from '../utils/AdmUtils';

/**
 *	----------------------------------------------------------------------------------------------
 *	Class AdmRouter - Handling app routing 
 * 	----------------------------------------------------------------------------------------------
 */
class AdmRouter extends AdmEventBus{

    /**
     * Create a Router.
     * @param {object} config - Configuration object of router
	 * @example 
	 * //Example 1:
	 * const config = {
	 *	root: '',
	 * 	routes: [{path, config, handler}],
	 * 	listen: true,
	 *  change: () => {}
	 * 	fail: () => {}
	 * 	initComplete =  () => {} || null;
	 * };
	 * 
	 * const router = new AdmRouter(config);
	 * 
	 * //Example 2:
	 * const router = new AdmRouter({root: ''});
	 * router
	 * 	.add("/", (routeData) => { ... }, configData)
	 * 	.add("/*", (routeData) => { ... }, configData)
	 * 	.add("*", (routeData) => { console.log("404"); })
	 * 	.listen()
     */
	constructor(config = {}){
		super();
		//	Public Properties
		this.root = config.root || '';
		this.path404 = config.path404 || '';
		this.routes = config.routes || [];
		this.initComplete = config.initComplete || null;

		//	Private Properties	
		this._routes = [];
		this._changeHandler = null;
		this._failHandler = null;
		this._lastRoute = null;

		//	Initialize routes
		if(config.routes) {
			config.routes.forEach(item => { this.add(item.path, item.config, item.handler); })
		}
		if(config.listen){
			this.listen();
		}
		if(config.change){
			this._changeHandler = config.change;
		}
		if(config.fail){
			this._failHandler = config.fail;
		}

		eventBus.addEventListener("navigate", (event) => {
			this.navigate(event.detail.path, event.detail.data, '', event.detail.replace);
		})
	}

    /**
     * Add a route.
     * @param {string} route - Expression of a route. For a catchall or 404 error route add an asteric ("*") as last route
	 * @param {object} config - Configuration of a route (optional)	 
	 * @param {function} handler - Handler by matching of a route (optional)
	 * @return {object} The router instance
     */
	add(route, config = null, handler = null){
		if(!route){ 
			throw new Error(`AdmRouter:add - Adding Route "${route}" failed - Invalid arguments`); 
		}
		const path = joinPath(this.root, route);
		route = new Route(path);
		this._routes.push({route, handler, config});
		return this;
	}

    /**
     * Remove a route.
     * @param {string} route - Expression of a route
	 * @return {object} The router instance
     */
	remove(route){
		const numRoutes = this._routes.length;
        for(let i = 0; i < numRoutes; i++) {
			const path = joinPath(this.root, route);
            if(this._routes[i].route.spec === path) {
				this._routes.splice(i, 1); 
				break;
            }
        }
		return this;
	}

    /**
     * Remove all routes.
	 * @return {object} The router instance
     */
	removeAll(){
		this._routes = [];
		return this;
	}

    /**
     * Listen to url changes.
	 * @return {object} The router instance
     */
	listen(){
		window.addEventListener('popstate', (event) => {
			const matchData = this.match(location.pathname + location.search, event.state);
			this._callRouteHandler(matchData);
		})
		return this;
	}

    /**
     * Match a route.
     * @param {string} path - Path from an url
	 * @param {string} data - ThroughPass data (optional)
	 * @return {object} Match data from route
     */
	match(path, data = null){
		let matchData = {
			route: null, 
			result: {
				path: path,
				pathParams: null,
				throughPassData: data,
				config: null,
				status: 404
			}
		};
		const numRoutes = this._routes.length;
		for(let i = 0; i < numRoutes; i++) {
			let match = this._routes[i].route.match(path);
			if(match){
				matchData.route = this._routes[i]
				matchData.result.status = 200;
				matchData.result.pathParams = match;
				matchData.result.config = Object.assign({}, this._routes[i].config);
				break;	
			}	
		}
		return matchData;
	}

    /**
     * Match a route.
     * @param {string} path - Path to navigate
	 * @param {string} data - ThroughPass data (optional)
	 * @param {string} title - Title from website (optional)
	 * @param {boolean} replace - History replace - default false (optional)
     */
	navigate(path, data = null, title = '', replace = false){
		if(path !== location.pathname + location.search){
			if(replace){
				window.history.replaceState(data, title, path);
			}else{
				window.history.pushState(data, title, path);
			}
		}
		const matchData = this.match(path, data);
		this._callRouteHandler(matchData);
	}

    /**
     * [Private] Handle route handlers.
     * @param {string} matchData - result of match function
     */
	_callRouteHandler(matchData){
		this.lastRoute = matchData.result;
		if(matchData?.route){
			if(matchData.route.handler){
				matchData.route.handler(matchData.result);
			}
			else if(this._changeHandler){
				this._changeHandler(matchData.result);
			}
			else{
				this.dispatchEvent("routeChanged", matchData.result);
			}
		}else{
			if(this._failHandler){
				this._failHandler(matchData.result);
			}
			else{
				this.dispatchEvent("routeFailed", matchData.result);
			}
		}
	}
}

export { AdmRouter }