Skip to content

Instantly share code, notes, and snippets.

@MrGrigri
Last active April 23, 2024 16:10
Show Gist options
  • Save MrGrigri/f717020a3cbde259aa81bbde3473ba90 to your computer and use it in GitHub Desktop.
Save MrGrigri/f717020a3cbde259aa81bbde3473ba90 to your computer and use it in GitHub Desktop.
Vanilla JavaScript Router

Simple vanilla JavaScript router

This is a very minimal router to be used in a vanilla JavaScript project. There are only two exposed functions and one options object.

I have used this with a simple ViteJs web server.

Functions

initializeRoutes(routes, options): This function will initialize the routing. A routes object must be passed as well as an optional options object. goToRoute(route): This function is to be called in JavaScript and will navigate to the route.

Types

Routes: Is an object where the property key is either a string or a number (e.g. '/about' or 404) and the property value is the path to the html file. RouterOptions: Is an object with two properties; rootSelector and linkSelector. Both of which are strings. They both default to [data-router-root] and [data-router-link] respectively.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vanilla JavaScript Router</title>
<script type="module" defer src="/main.js"></script>
</head>
<body>
<nav>
<h1>My Web Site</h1>
<ul>
<li><a data-router-link href="/">Home</a></li>
<li><a data-router-link href="/about">About</a></li>
</ul>
</nav>
<main id="app" data-router-root></main>
</body>
</html>
import { initializeRoutes } from "./router.js";
/** @type {string} */
const BASE_ROUTES = "./src/routes";
/** @type {import("./router.js").Routes} */
const ROUTES = {
404: `${BASE_ROUTES}/404.html`,
"/": `${BASE_ROUTES}/home.html`,
"/about": `${BASE_ROUTES}/about.html`,
};
initializeRoutes(ROUTES);
/**
* @typedef {Record<string | number, string>} Routes
*/
/**
* @typedef {Object} RouterOptions
*
* @property {string} rootSelector
* @property {string} linkSelector
*/
/** @type {Routes} */
let _routes;
/** @type {RouterOptions} */
let _routerOptions;
/** @type {Array<HTMLAnchorElement>} */
let _routerLinks;
/** @type {RouterOptions} */
const _defaultRouterOptions = {
rootSelector: '[data-router-root]',
linkSelector: '[data-router-link]',
};
/**
* Navigate to a specific route
* @param {string} [route] - Route to navigate to
* @returns {Promise<boolean | Error>}
* @throws {Error} - When root selector is not found
* @throws {Error} - When route is not found
*/
const _goToRoute = async (route) => {
try {
const raw = await fetch(route);
const html = await raw.text();
const rootElement = document.querySelector(_routerOptions.rootSelector);
if (!rootElement) {
throw Error(
`Unable to find element with css selector: ${_routerOptions.rootSelector}`
);
}
rootElement.innerHTML = html;
return true;
} catch {
throw Error(`Unable to get html route: ${_routes[route]}`);
}
};
/**
* Handle the navigation change
* @returns {Promise<boolean>}
*/
const _handleLocationChange = async () => {
const path = window.location.pathname;
const route = _routes[path] || _routes['404'];
return _goToRoute(route);
};
/**
* Get an array of HTML anchor elements
* @param {string} [selector] - CSS selector to find the router links
* @returns {Array<HTMLAnchorElement>}
*/
const _getRouterLinks = (selector) => {
const links = document.querySelectorAll(selector);
if (!links)
throw Error(
`Unable to find element with css selector: ${_routerOptions.rootSelector}`
);
return [...links];
};
/**
* Handles the click handler for the router link
* @param {MouseEvent} [e] - MouseEvent on click of router link
*/
const _handleRouterLinkClick = (e) => {
e.preventDefault();
/** @type {HTMLAnchorElement} [target] */
const target = e.target;
window.history.pushState({}, '', target.href);
_handleLocationChange();
};
/**
* Sets and handles the window popstate event
*/
const _addPopstateEvent = () => {
window.addEventListener('popstate', _handleLocationChange);
};
/**
* Sets and handles the router link click events
*/
const _addRouterLinkEvents = () => {
_routerLinks.forEach((route) => {
route.addEventListener('click', _handleRouterLinkClick);
});
};
/**
* Main export to initialize the routes
* @param {Routes} [routes] - Routes
* @param {RouterOptions} [options] - Options for routes
* @returns {Promise<boolean>}
*/
export const initializeRoutes = async (
routes,
options = _defaultRouterOptions
) => {
_routes = routes;
_routerOptions = options;
_routerLinks = _getRouterLinks(_routerOptions.linkSelector);
_addPopstateEvent();
_addRouterLinkEvents();
return await _handleLocationChange();
};
/**
* Go to a specific route
* @param {string} [path]
* @returns {Promise<boolean>}
*/
export const goToRoute = (route) => {
const routeToGoTo = _routes[route] || _routes['404'];
return _goToRoute(routeToGoTo);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment