Examples

Autonomous Router Component

A simple router to give push state or hash style URL based loading of page components. Divert the URL to a route to load the page component inside the router. Using many attributes to set routes, the route thats loaded and configure the router. Used mainly inside application components or other frameworks. Bootstraped from a bootstrap file, by importing into a bigger application or direct as html script (ensure you build fallback for IE support).

<my-router></my-router>

MyRouter Usage <my-router>


<!-- Polyfill -->
<script src="/node_modules/promise-polyfill/dist/polyfill.min.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>

<!-- Bootstrap component only, ensure you have built fallback for IE support too (.js)! -->
<script type="module" src="./your/path/my-switch.mjs"></script>
<script nomodule src="./your/path/my-switch.js"></script>

<!-- Or add your switch import to a bootstrap js file/app using ES6 import and use that instead of the component directly... -->
<script type="module" src="./your/path/index.mjs"></script>
<script nomodule src="./your/path/index.js"></script>

...

<div class="info">
	<div class="class">
		<div class="row" fouc>
			<div class="col-sm-4">
				<my-router .route="${this.route}" .routes="${this.routes}" default="test" not-found="404" push-state redirect></my-router>
			</div>
		</div>
	</div>
</div>

...
						

MyRouter Class my-router.mjs


import { CustomHTMLElement, html } from '../../../node_modules/custom-web-component/index.js';

/**
 * @public @name MyRouter
 * @extends CustomHTMLElement
 * @description Custom Web Component, adds dynamic lazy routing (deactivated) via push state URL or hashtag
 * @author Paul Smith <[email protected]>
 * @copyright 2018 Paul Smith (ulsmith.net)
 * @license MIT
 * 
 * @property {String} route The route to set as 'one', 'one/two'
 * @property {Array} routes The routes to use as array of objects [{src: '../../app/test/app-test-index.js', component: 'app-test-index', route: 'test' },...]
 * 
 * @attribute default The default route to use for no route (index page)
 * @attribute not-found The route to use when no route found (404)
 * @attribute push-state Flag to tell the router if it's using push-state (fallback is hashtag) 
 * @attribute redirect Flag to tell the system to redirect the default page to it's actual route
 * 
 * @example HTML
 * <my-router .route="${this.route}" .routes="${this.routes}" default="test" not-found="404" push-state redirect></my-router>
 */
class MyRouter extends CustomHTMLElement {
	/**
	 * @public @constructor @name constructor
	 * @description Process called function triggered when component is instantiated (but not ready or in DOM, must call super() first)
	 */
	constructor() {
		super();

		// properties
		this.route;
		this.routes;

		// private
		this._windowEvent;
		this._selected;
		this.style.display = 'block';
	}

	/**
	 * @public @name template
	 * @description Template function to return web component UI
	 * 
	 * @return {String} HTML template block
	 * 
	 * @example JS
	 * this.updateTemplate(); // updates the template and topically re-renders changes
	 */
	static template() {
		return html`<div id="my-router"></div>`;
	}

	/**
	 * @public @static @get @name observedProperties
	 * @description Lifecycle hook that sets properties to observe on the element
	 * 
	 * @return {Array} An array of string property names (camelcase)
	 */
	static get observedProperties() {
		return ['route', 'routes'];
	}

	/**
	 * @public @static @get @name observedAttributes
	 * @description Lifecycle hook that sets attributes to observe on the element
	 * 
	 * @return {Array} An array of string attribute names (hyphoned)
	 */
	static get observedAttributes() {
		return ['default', 'not-found', 'push-state', 'redirect'];
	}

	/**
	 * @public @name connected
	 * @description Lifecycle hook that gets called when the element is added to DOM
	 */
	connected() {
		this._windowEvent = window.addEventListener('popstate', this.updateRoute.bind(this));
	}

	/**
	 * @public @name disconnected
	 * @description Lifecycle hook that gets called when the element is removed from DOM
	 */
	disconnected() {
		window.removeEventListener('popstate', this._windowEvent);
	}

	/**
	 * @public @name propertyChanged
	 * @description Lifecycle hook that gets called when the elements observed properties change
     * 
	 * @param {String} property Name of the property changed
     * @param {Mixed} oldValue Value before the change
     * @param {Mixed} newValue Value after the change
	 */
	propertyChanged(property, oldValue, newValue) {
		if (property === 'route' && this.routes) this.updateRoute(newValue);
	}

	/**
	 * @public @name templateUpdated
	 * @description Lifecycle hook that gets called when the elements template is updated on DOM
	 */
	templateUpdated() {
		this.updateRoute();
	}

	/**
	 * @public @name updateRoute
	 * @description Update the router with a new route
     * 
	 * @param {Mixed} data The new route as a route object ({component: 'some-thing', route: 'one'}), or a route string 'one'
	 */
	updateRoute(data) {
		let path;

		// empty route or popstate event, work out route from url | route passed in as route | route passed as string path string
		if (!data || data.type === 'popstate') path = this.hasAttribute('push-state') ? window.location.pathname.replace(/^\/|\/$/g, '') : window.location.hash.replace(/^\#|\#$/g, '');
		else if (data.path !== undefined) path = data.path.replace(/^\/|\/$/g, '');
		else if (data.length) path = data.replace(/^\/|\/$/g, '');

		// resolve from path
		let route = this._getRouteFromPath(path);
		let routeDefault = this._getRouteFromPath(this.getAttribute('default'));
		let routeNotFound = this._getRouteFromPath(this.getAttribute('not-found'));

		// not set, load default, set then load, else 404
		if (!path) this._loadRoute(routeDefault);
		else if (route) this._loadRoute(route);
		else this._loadRoute(routeNotFound);
	}

	/**
	 * @public @name _getRouteFromPath
	 * @description Get the router object from the list of routes using the path to search
     * 
	 * @param {String} path The path to search for in the routes array
	 * 
	 * @return {Object} The route object
	 */
	_getRouteFromPath(path) {
		return this.routes.filter(selected => selected.path === path)[0] || undefined;
	}

	/**
	 * @public @name _loadRoute
	 * @description Load the route from the selected route object. Dynamic lazy imports is deactivated
     * 
	 * @param {Object} selected The route object to load the route from
	 */
	_loadRoute(selected) {
		// should we load route, has it changed
		if (this._selected && this._selected.component == selected.component) return;

		// set route
		this._selected = selected;
		this.route = selected.path;

		if (!customElements.get(selected.component)) {
			// this is for when modules importing is universally excepted
			// import(selected.src).then(() => this._paintRoute(selected.component));
		} else this._paintRoute(selected.component);
		
		// persist history/location
		if (this.hasAttribute('push-state')) {
			// should we redirect default to path
			let path = this._selected.path !== this.getAttribute('default') ? this._selected.path : (this.hasAttribute('redirect') ? this._selected.path : '');
			history.pushState({ 'route': path }, '', path);
		} else window.location.hash = this._selected.path;
	}

	/**
	 * @public @name _paintRoute
	 * @description Paint the loaded component to screen, omits event once complete
     * 
	 * @param {String} component The component tag name (lowercase, hyphoned)
	 */
	_paintRoute(component) {
		this.dispatchEvent(new CustomEvent('startchange', { detail: this._selected }));
		this.style.minHeight = this.offsetHeight + 'px';
		this.style.opacity = 0;
		setTimeout(() => {
			this.dispatchEvent(new CustomEvent('routechange', { detail: this._selected }));
			this.shadowRoot.innerHTML = `<${component}></${component}>`;
			setTimeout(() => {
				this.style.opacity = 1;
				this.style.minHeight = '0px';
				this.dispatchEvent(new CustomEvent('change', { detail: this._selected }));
			}, 100);
		}, 100);
	}
}

// define the new custom element
customElements.define('my-router', MyRouter);