Page Router Demo

A simple simulation of routing to different URLs while staying on the same page, providing view objects instead of actual pages.

This is a demo of the page router for single-page applications. It is a tiny SPA, made up of four pages.

When the browser routes to a page, the PageRouter class determines, based on the route configuration, what component or element to return, and then places it into the <page-router> element in the HTML. By default, the PageRouter class determines what to return by using the URL hash, the text following the pound sign (#) in the URL, which can be changed without forcing a page reload. This is not required, however, as you can choose to route only using the PageRouter.route() method.

PageRouter type definition
IRoute type definition

All methods of the PageRouter class are static methods that can be accessed from anywhere. The first method you must call is always PageRouter.configure(), which does initial set-up of the router, the kind of thing that in .Net would be handled by a static constructor. In the demo script, this call appears at the end.

mi5.router.PageRouter.configure([
    { route: 'login', payload: LoginView },
    { route: 'home', payload: ChoiceView, routeGuards: loginGuard },
    { route: 'door/3', payload: doorThree, routeGuards: loginGuard },
    { route: 'door/:id', payload: door, routeGuards: loginGuard },
], kw('defaultRoute', 'login'));

The configure method can take a list of routes (you can also call addRoute() one by one). This page has four routes. It also sets the defaultRoute to login, which means that if you load the page with no hash, it will route to the default page.

The syntax kw('defaultRoute', 'login') is using the keyword argument helper, which is described in the keyword argument demo. If you've ever used C#, this would be the same as if you included defaultRoute: "login" in your method call. In Python, defaultRoute = "login". In JS, you could get the same result by calling PageRouter.configure([], undefined, undefined, undefined, 'login') ... which is pretty wordy and opaque.

Each route to be added to the PageRouter must have a route, which is a string to be matched, and a payload. The payload can be a component class or a parameterless function that returns a constructed component or a HTML element. It can also be an existing component; in this case, the component is cloned, so bugs aren't created when an existing component is suddenly moved to a new location.

When you return a component or element from a function, there isn't really a way to enforce that the element returned is new. If you return an existing element, you will see either errors or bugs, so don't do it.

This demo page has four sub-pages, four routes. The first of four routes is the login page, provided by the LoginView class, which inherits from the Component class. I will skip most of the details of the login component; if you're interested in more details about components, check out to the component API pages or one of the component demos. There only point that is relevant to routing is in the login() method. When the user logs in with user name and password, if they are correct, it routes the user to the 'home' route.

The username can be anything. The password is "password" because security is important to us. Just as important as it is to all corporations that have your personal data.

if (data.userName && data.password === 'password') {
    sessionStorage.setItem('userName', data.userName);
    mi5.router.PageRouter.route('home');
}

In place of an auth cookie, this uses sessionStorage, which is dead simple and very easy to clear. If the user is logged in, 'userName' is populated. After setting the login state, the view calls the PageRouter.route() method to route to home. Alternately, it could have changed window.location.hash.

This example uses sessionStorage to store the logged-in state, because it's mega simple and vanishes as soon as you close the tab or the browser. Unfortunately this breaks this demo in Firefox, because Firefox responds with "SecurityError: The operation is insecure." So view the demo in Chrome.

The second route, the home page, returns ChoiceView, which does nothing more than show the logged-in user's name and request that the user choose between two doors.

Notice that the payload in these cases is returning the component class, not an instance of the component. Also notice the the constructors take zero arguments.

The third route, door/3, is a special case that appears if the user goes into the URL bar and changes the path to door/3. This is a simple static div, constructed using Ichigo's div helper (see the relevant demo). It must appear before the next route because this is a more specific version of the same thing. The rule when there are multiple routes that match the current URL is first come, first served.

The final route, door/:id, contains a variable parameter, id, which can match any string. The payload in this case is a function, door(), which gets the value of the id parameter, using PageRouter.params.get('id'), and returns a div whose content is determined by the user's choice.

function door() {
    switch (mi5.router.PageRouter.params.get('id')) {
        case '1':
            return mi5.html.div('Lady', { classList: 'text-center alert alert-success' });
        case '2':
            return mi5.html.div('Tiger', { classList: 'text-center alert alert-warning' });
        default:
            return mi5.html.div('INVALID DOOR', { classList: 'text-center alert alert-dark' })
} }

The final piece of code is the login guard, which checks to see if the user is logged in, and if the user is not logged it, redirects the user and returns false. This is set on every route but login, which prevents users that aren't logged in from seeing anything but the login page.

IRouteGuard type definition

Route guards must be objects that implement IRouteGuard. The only requirement to be a route guard is to have a function named checkValid that takes the route as an input and returns either true (allowed to route to the page) or false (not allowed to route to the page). They can do whatever else they need to, and being objects, can be constructed with whatever properties they need to work, such as a list of user roles that may see a page.

const loginGuard = {
    checkValid: function(route) {
    const loggedIn = sessionStorage.getItem('userName');
    if (loggedIn) {
        return true;
    }
    mi5.router.PageRouter.route('login');
    return false;
} };

Link to the script