Guide on implementing vanilla JavaScript routing for single page applications

I am in the process of developing a web application using only Vanilla JavaScript, without any frameworks or libraries. I am following more of a 'React' style approach.

My goal is to load a view from my views/pages/dashboard.js file, display that view, and update the URL when the user clicks on the dashboard navigation link. Here is the navbar: https://codepen.io/Aurelian/pen/EGJvZW.

I am considering integrating the sub-navigation items into the routing system. If the user is in the GitHub folder under profile, how can I reflect that in the URL?

How can I implement routing for this scenario?

You can find the GitHub repository here: https://github.com/AurelianSpodarec/JS_GitHub_Replica/tree/master/src/js

This is my current attempt:

document.addEventListener("DOMContentLoaded", function() {
    var Router = function (name, routes) {
        return {
            name: name,
            routes: routes
        }
    };
    var view = document.getElementsByClassName('main-container');
    var myRouter = new Router('myRouter', [
        {
            path: '/',
            name: "Dashboard"
        },
        {
            path: '/todo',
            name: "To-Do"
        },
        {
            path: '/calendar',
            name: "Calendar"
        }
    ]);
    var currentPath = window.location.pathname;
    if (currentPath === '/') {
        view.innerHTML = "You are on the Dashboard";
        console.log(view);
    } else {
        view.innerHTML = "you are not";
    }
});

Answer №1

When creating a vanilla single page application (SPA), there are at least two main approaches to consider.

Hash Router

The first approach involves adding a listener to the window.onhashchange event, which triggers whenever the hash in the URL changes, allowing you to interpret the route based on the hash value and inject the appropriate content.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <div id="app"></div>
    <script>
      // JavaScript code for hash router implementation
    </script>
  </body>
</html>

History API

The second approach utilizes the History API, providing a more seamless user experience by removing the need for hash characters in the URL.

This strategy involves listening for same-domain link clicks and utilizing window.history.pushState to update the browser history based on the target URL.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <div id="app"></div>
    <script>
      // JavaScript code for History API implementation
    </script>
  </body>
</html>

For more advanced routing capabilities, the use of tools like the route-parser package is recommended to streamline development.

Without JS

Additionally, an alternative method to create a hash-based SPA without using JavaScript involves leveraging CSS techniques like the :target pseudoselector to toggle content visibility within a single HTML file.

// CSS code for building a hash-based SPA without JS
HTML and inline CSS elements...

Answer №2

Utilizing the popstate event and the hashtag (#) method, as mentioned in the comments, is a simple approach to routing in JavaScript.

Below is a basic structure for a router:

//App area
var appArea = document.body.appendChild(document.createElement("div"));
//Registered routes
var routes = [
    {
        url: '', callback: function () {
            appArea.innerHTML = "<h1>Home</h1><a href=\"#todo\">To-Do</a><br/><a href=\"#calendar\">Calendar</a>";
        }
    }
];
//Routing function
function Routing() {
    var hash = window.location.hash.substr(1).replace(/\//ig, '/');
    //Default route is first registered route
    var route = routes[0];
    //Find matching route
    for (var index = 0; index < routes.length; index++) {
        var testRoute = routes[index];
        if (hash == testRoute.url) {
            route = testRoute;
        }
    }
    //Fire route
    route.callback();
}
//Listener
window.addEventListener('popstate', Routing);
//Initial call
setTimeout(Routing, 0);
//Add other routes
routes.push({ url: "todo", callback: function () { appArea.innerHTML = "<h1>To-Do</h1><a href=\"#\">Home</a><br/><a href=\"#calendar\">Calendar</a>"; } });
routes.push({ url: "calendar", callback: function () { appArea.innerHTML = "<h1>Calendar</h1><a href=\"#\">Home</a></br><a href=\"#todo\">To-Do</a>"; } });

If implemented in a real scenario, you would need reusable DOM elements and scope-unload functions. Here's an enhanced version of the above logic:

// ## Class ## //
var Router = /** @class */ (function () {
    function Router() {
    }
    //Initializer function. Call this to change listening for window changes.
    Router.init = function () {
        //Remove previous event listener if set
        if (this.listener !== null) {
            window.removeEventListener('popstate', this.listener);
            this.listener = null;
        }
        //Set new listener for "popstate"
        this.listener = window.addEventListener('popstate', function () {
            //Callback to Route checker on window state change
            this.checkRoute.call(this);
        }.bind(this));
        //Call initial routing as soon as thread is available
        setTimeout(function () {
            this.checkRoute.call(this);
        }.bind(this), 0);
        return this;
    };
    //Adding a route to the list
    Router.addRoute = function (name, url, cb) {
        var route = this.routes.find(function (r) { return r.name === name; });
        url = url.replace(/\//ig, '/');
        if (route === undefined) {
            this.routes.push({
                callback: cb,
                name: name.toString().toLowerCase(),
                url: url
            });
        }
        else {
            route.callback = cb;
            route.url = url;
        }
        return this;
    };
    //Adding multiple routes to list
    Router.addRoutes = function (routes) {
        var _this = this;
        if (routes === undefined) { routes = []; }
        routes
            .forEach(function (route) {
            _this.addRoute(route.name, route.url, route.callback);
        });
        return this;
    };
    //Removing a route from the list by route name
    Router.removeRoute = function (name) {
        name = name.toString().toLowerCase();
        this.routes = this.routes
            .filter(function (route) {
            return route.name != name;
        });
        return this;
    };
    //Check which route to activate
    Router.checkRoute = function () {
        //Get hash
        var hash = window.location.hash.substr(1).replace(/\//ig, '/');
        //Default to first registered route. This should probably be your 404 page.
        var route = this.routes[0];
        //Check each route
        for (var routeIndex = 0; routeIndex < this.routes.length; routeIndex++) {
            var routeToTest = this.routes[routeIndex];
            if (routeToTest.url == hash) {
                route = routeToTest;
                break;
            }
        }
        //Run all destroy tasks
        this.scopeDestroyTasks
            .forEach(function (task) {
            task();
        });
        //Reset destroy task list
        this.scopeDestroyTasks = [];
        //Fire route callback
        route.callback.call(window);
    };
    //Register scope destroy tasks
    Router.onScopeDestroy = function (cb) {
        this.scopeDestroyTasks.push(cb);
        return this;
    };
    //Tasks to perform when view changes
    Router.scopeDestroyTasks = [];
    //Registered Routes
    Router.routes = [];
    //Listener handle for window events
    Router.listener = null;
    Router.scopeDestroyTaskID = 0;
    return Router;
}());
// ## Implementation ## //
//Router area
var appArea = document.body.appendChild(document.createElement("div"));
//Start router when content is loaded
document.addEventListener("DOMContentLoaded", function () {
    Router.init();
});
//Add dashboard route
Router.addRoute("dashboard", "", (function dashboardController() {
    //Scope specific elements
    var header = document.createElement("h1");
    header.textContent = "Dashboard";
    //Return initializer function
    return function initialize() {
        //Apply route
        appArea.appendChild(header);
        //Destroy elements on exit
        Router.onScopeDestroy(dashboardExitController);
    };
    //Unloading function
    function dashboardExitController() {
        appArea.removeChild(header);
    }
})());
//Add dashboard route
Router.addRoute("dashboard", "", (function dashboardController() {
    //Scope specific elements
    var header = document.createElement("h1");
    header.textContent = "Dashboard";
    var links = document.createElement("ol");
    links.innerHTML = "<li><a href=\"#todo\">To-Do</a></li><li><a href=\"#calendar\">Calendar</a></li>";
    //Return initializer function
    return function initialize() {
        //Apply route
        appArea.appendChild(header);
        appArea.appendChild(links);
        //Destroy elements on exit
        Router.onScopeDestroy(dashboardExitController);
    };
    //Unloading function
    function dashboardExitController() {
        appArea.removeChild(header);
        appArea.removeChild(links);
    }
})());
//Add other routes
Router.addRoutes([
    {
        name: "todo",
        url: "todo",
        callback: (function todoController() {
            //Scope specific elements
            var header = document.createElement("h1");
            header.textContent = "To-do";
            var links = document.createElement("ol");
            links.innerHTML = "<li><a href=\"#\">Dashboard</a></li><li><a href=\"#calendar\">Calendar</a></li>";
            //Return initializer function
            return function initialize() {
                //Apply route
                appArea.appendChild(header);
                appArea.appendChild(links);
                //Destroy elements on exit
                Router.onScopeDestroy(todoExitController);
            };
            //Unloading function
            function todoExitController() {
                appArea.removeChild(header);
                appArea.removeChild(links);
            }
        })()
    },
    {
        name: "calendar",
        url: "calendar",
        callback: (function calendarController() {
            //Scope specific elements
            var header = document.createElement("h1");
            header.textContent = "Calendar";
            var links = document.createElement("ol");
            links.innerHTML = "<li><a href=\"#\">Dashboard</a></li><li><a href=\"#todo\">To-Do</a></li>";
            //Return initializer function
            return function initialize() {
                //Apply route
                appArea.appendChild(header);
                appArea.appendChild(links);
                //Destroy elements on exit
                Router.onScopeDestroy(calendarExitController);
            };
            //Unloading function
            function calendarExitController() {
                appArea.removeChild(header);
                appArea.removeChild(links);
            }
        })()
    }
]);

Answer №3

Explore new possibilities with navigo or gain inspiration by observing the innovative work of others.

If you're looking for an alternative to React/Angular, consider utilizing sapper and delve into a comprehensive comparison of frameworks from this source.

In my view, a router should be versatile, not only managing existing application components but also handling server requests and AJAX responses for dynamic page content. For example, a request for /eshop/phones/samsung would trigger an AJAX call to include HTML code within a specified node like <div id="eshop">. This approach requires:

1) A URL handler to intercept clicks and modify browser paths.

2) A callback function to determine actions based on the modified path.

This strategy ensures SEO optimization by mapping URLs to cached pages, while dynamically constructed pages are generated as needed. It's important to note that network crawlers may require JS execution to parse dynamically loaded content; however, the router facilitates bookmarking functionality even if indexing remains challenging.

By incorporating SEO and bookmarking capabilities into the router, it offers a unique advantage compared to more complex frameworks like Angular. This streamlined approach simplifies project reuse and seamlessly integrates SPAs with server-rendered content.

In essence, this router mirrors a server-side routing structure by managing both cached and dynamically generated content URLs, striking a balance between SPA functionalities and traditional server-rendered pages.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Having trouble establishing a connection to the FTP server through the "ftp" package provided by npm

Attempting to establish a connection to a Secured FTP server using the "ftp" package. When connecting to an unsecured server, everything functions as expected with all events firing and content being displayed. However, upon trying to connect to a server ...

What is the process for converting monthly data into an array?

I am working with an array of daily data that looks like this: var data = [{x: '2017-01-01', y: 100}, {x: '2017-01-02', y: 99}, /* entire year. */]; Each element in the array has an x field for date and a y field for a number. This ar ...

The p5.play library does not function properly on repl.it

I've recently started using p5.play, but I keep encountering this error whenever I try to run a program (I'm using repl.it); p5 is having issues creating the global function "Animation", possibly because your code already uses that name as a va ...

Guide to configuring the initial lookAt/target for a Control

I'm looking to establish the initial 'lookAt' point for my scene, which will serve as both the center of the screen and the rotation control's focus. Ideally, I'd like to set a specific point or object position rather than rotation ...

The delimiting slash is absent between hosts and options

Hello there (I hope my English is not too bad)! Currently, I'm attempting to set up a basic database using MongoDB Atlas (the online alternative), but I am facing an issue at the very first step: connecting! Every time I try, I encounter the same err ...

Issue in React Native: Undefined value is being referred to as an object when evaluating 'Object.keys(dataBlob[sectionID])'

As I work on my React Native application, I encountered a challenge when trying to display Facebook page status using the Facebook API in a ListView. Thankfully, this tutorial provided me with valuable insight and allowed me to successfully display the lat ...

Can a dynamic import from a Node module be exported?

I have developed an npm package that utilizes a dynamic import(). This package is written in TypeScript and compiled with the module: "esnext" compiler option, which means the import() call remains unchanged in the output. The expectation was to load this ...

Creating a Unique Flot Bar Chart Experience

I am currently working on creating a bar chart and have the following requirements: Bar labels for each data point Centering the bars in the grid Below is my jQuery code snippet: var data = [[0,206],[1,118],[2,37]]; var dataset = [ { labe ...

transforming json data into an array or map

Once my REST API responds, it provides the following JSON content: [{ "key": "apple", "value": "green" }, { "key": "banana", "value": "yellow" }] Managing the data, I use the following code to iterate through the list: this.props.json.ma ...

Why won't the dynamic button trigger the JavaScript function?

I've been adding a button dynamically to my page using the following code snippet. if (adminInd =='X'){ var adminDiv = $( '<label id=Delegatelbl>Miscellaneous label prototyping.:</label>'+ '& ...

jquery-validation error in gulp automations

I've encountered a sudden error in my gulp build related to jQuery validation. There haven't been any recent changes that would trigger this issue. Interestingly, one of my colleagues is experiencing the same problem, while another one is not. We ...

Populating a multi-layered array using JSON data

My goal with this code is to populate a global array with JSON objects. // declaring global arrays var sectionL=[1,5,3,7,5,4,3,3,4,4,6,6,3,5,5,1]; var sectionsTitle=new Array(16); var sectionsContent=new Array(16); for(var i=0; i<sectionL.length;i++){ ...

What is the best way to prevent text from being dragged within a contenteditable div?

Currently, I am working on a contenteditable div. This div has a default behavior that allows you to drag pieces of text around. I have tested this behavior on Chrome/Windows only so far. https://i.sstatic.net/IWLsY.gif I am looking for a way to disable ...

Stop unauthorized access to php files when submitting a contact form

I have implemented a contact form on my HTML page that sends an email via a PHP script upon submission. However, when the form is submitted, the PHP script opens in a new page instead of staying on the current page where the form resides. I have tried usin ...

Etiquette for the organization of jQuery functions through chaining and nesting

For developers familiar with jQuery, creating dynamic HTML easily can be done using the following method: var d = $('<div/>') .append('some text') .append( $('<ul/>').append( $('&l ...

Incorporating Meteor js into an established user system

I'm completely new to the world of Meteor and I am looking to integrate it with my current system that relies on a MongoDB database. As I explore Meteor, I have discovered that there are packages like accounts-facebook and accounts-twitter which assis ...

Transfer the content from a text area to a div element while keeping the original line breaks

I have a textarea and a div (containing a paragraph tag) where the content of the paragraph tag is updated with the text entered into the textarea upon keyUp. Is there a way to maintain line breaks from the textarea and have them appear as line breaks in ...

Setting up anchor tags with dynamically changing href values does not trigger a get request

I seem to be facing an issue that I can't quite pinpoint. Essentially, I am retrieving data from my database to populate an HTML page and dynamically assigning href values to some anchor tags. However, upon clicking on the links, the page simply reloa ...

Having issues with jQuery's .text() method not functioning as expected on XML elements

Looking at the javascript code below: function getAdminMessageFromXML(xml) { alert('xml: ' + xml); alert("Text of admin message: " + $(xml).find('dataModelResponse').find('adminMessage').text()); return $(xml).fin ...

Asynchronous requests in Node.js within an Array.forEach loop not finishing execution prior to writing a JSON file

I have developed a web scraping Node.js application that extracts job description text from multiple URLs. Currently, I am working with an array of job objects called jobObj. The code iterates through each URL, sends a request for HTML content, uses the Ch ...