javascriptecmascript-6routessingle-page-application

How to create a vanilla JS routing for SPA?


I'm creating a web app with no frameworks/tools/libraries, all Vanilla JS. I'm doing it in more of a 'React' style.

I'd like to call a view that is in my views/pages/dashboard.js, display that view and change the URL when the user clicks the dashboard nav link. This is the navbar: https://codepen.io/Aurelian/pen/EGJvZW.

Perhaps it'd be nice to integrate the sub-nav items into the routing. What if the user is in the GitHub folder on profile, how would I display that in the URL as well?

How can I create a routing for this?

The GitHub repo is https://github.com/AurelianSpodarec/JS_GitHub_Replica/tree/master/src/js

This is what I've tried:

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: "Dahsboard"
        },
        {
            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";
    }
});

Solution

  • As i said in the comments, listening for popstate and using the hashtag (#) method is the easiest way to do routing in JS.

    This is the most bare bones 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>"; } });

    Now in any real context you would want reusable DOM elements and scope-unload functions so here is how the above should probably look:

    // ## 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 === void 0) {
                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 === void 0) { 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);
                }
            })()
        }
    ]);