Intro to the Dojo Router

We took a quick look at the Dojo router when we reviewed the template application from the dojo cli. The template application provides almost everything you need to know about the Dojo router. But let’s take a little deeper look at routing.

Defining Routes

The template application does a great job of providing a clear way to configure your routes.

export default [
  {
    path: "home",
    outlet: "home",
    defaultRoute: true
  },
  {
    path: "about",
    outlet: "about"
  },
  {
    path: "profile/{username}",
    outlet: "profile"
  }
];

The routes are defined as an array of objects. each Route object has a RouteConfig interface with properties you can define. In the snippet above I have made one change. I have set the path for the profile route as profile/{username}. This means I will need to define a parameter to that route, which we’ll get to in a moment, but first let’s look at the options for a route config.

// dojo/framework/src/routing/interfaces.d.ts
export interface RouteConfig {
  path: string;
  outlet: string;
  children?: RouteConfig[];
  defaultParams?: Params;
  defaultRoute?: boolean;
}

That’s the beauty of working with TypeScript and Dojo, you can look at the types and interfaces of the code and use them as a guide as to how you should use the tools. The only required properties are path and outlet. One of the other properties we see defined in our configuration is the defaultRoute, which as you may have guessed is the default route of your application. Who says naming things is hard?!

The children property would be used if you had nested routes. You could also define some default parameters, which is really useful if you have a route the depends on parameters, and your route needs them to behave correctly.

Outlet

The first part of routing we need to look at is the Outlet. The Outlet is a higher order component that you use to wrap up widgets that are part of a designated route.

// src/App.ts
...
export default class App extends WidgetBase {
  protected render() {
    return v("div", { classes: [css.root] }, [
      w(Menu, {}),
      v("div", [
        // Outlet is where routes go
        // the decide which widgets
        // match each route
        w(Outlet, {
          key: "home",
          id: "home",
          renderer: () => w(Home, {})
        }),
        w(Outlet, {
          key: "about",
          id: "about",
          renderer: () => w(About, {})
        }),
        w(Outlet, {
          key: "profile",
          id: "profile",
          renderer: () => w(Profile, { username: "Dojo User" })
        })
      ])
    ]);
  }
}

Looking at the outlet, you can see that we define the id of the Outlet to match the Route configuration we defined. The actual widget rendered in the Route doesn’t have to match the id, but as you can see, it’s pretty good practice to do so. Keep the code readable please.

Outlets are pretty straightforward. Since they render the widget for a Route, they can also handle passing any URL parameters as properties to the widget.

Link and Parameters

Before we dive in to URL parameters, first we need to talk about how you can create a link to a route that is expecting parameters. We can define those parameters with a specific component in Dojo for working with routes, the Link component.

// src/widgets/Menu.ts
w(
  Link,
  {
    to: 'profile',
    key: 'profile',
    classes: [css.link],
    activeClasses: [css.selected],
    params: {
      username: 'odoe'
    }
  },
  ['Profile']
)

The Link component is designed specifically for creating links to routes and static paths in your application. They provide some sugar to regular anchor tags you can take advantage of in your apps. In this case, I am providing a value to the username parameter we defined for our route. This means that is will pass the object { username: ‘odoe’ } to my Outlet that I can then use to pass to my child widget.

// src/App.ts
w(Outlet, {
  key: 'profile',
  id: 'profile',
  renderer: ({ params }: MatchDetails) => {
    return w(Profile, { username: params.username });
  }
})

When you pass parameters to a URL in the Dojo router, your render method is passed the parameters for you to use in your application as needed. Now, although this method works fine, you can be more explicit in how you use your route parameters.

You can define query parameters in your routes and use them for more advanced usage. Let’s update the route configuration.

// src/routes.ts
export default [
  ...
  {
    path: "profile/{param}?{value}",
    outlet: "profile"
  }
];

Maybe we have different ways of searching for users in our backend API. We can search by name or id,.

// src/widgets/Menu.ts
w(
  Link,
  {
    to: 'profile',
    key: 'profile',
    classes: [css.link],
    activeClasses: [css.selected],
    params: {
      param: 'name',
      value: 'odoe'
    }
  },
  ['Profile']
)

Now we can update our Outlet to pass the correct information to the child widget.

// src/App.ts
w(Outlet, {
  key: 'profile',
  id: 'profile',
  renderer: ({ params, queryParams }: MatchDetails) => {
    const user = users.find((user: User) => {
      return user[params.param] == queryParams.value;
    }) as User;
    return w(Profile, { username: `${user.name} ${user.lastName}` });
  }
})

Now we have built a fairly generic way of passing parameters and values to our Outlet and being able to search for the correct username to use in our widget. We can search by the name value or an id value.

Default Parameters

So far we have been defining parameters in our Link, but maybe we want to define some default parameters directly in our route instead.

// src/routes.ts
export default [
  ...
  {
    path: 'profile/{param}?{value}',
    outlet: 'profile',
    defaultParams: {
      param: 'id',
      value: '2'
    }
  }
];

For out default route, we can decide to search by id with a value of 2. When you start dealing with URL parameters, everything is a string, so if you wanted to use real numbers, you would need to do some additional sanitization in your application, but I think we’ve dived pretty deep into setting up the Dojo router for starter use. Big thanks to Anthony Gubler for helping me out with some of my router questions, it was a big help.

Summary

As you can see, the Dojo router is very flexible in how you want to define your routes and parameters. Depending on how your backend APIs are defined, you could create some very powerful and scalable applications!

Dojo CLI Template App

dojo cli template application

The release of Dojo 4 introduced some really nice new features in their build pipeline to optimize for progressive web apps, some performance improvements under the hood in their rendering engine, and more.

However, one of the really cool things I haven’t seen talked about too much is the new template application you get with the dojo cli. You can check out my earlier post on using the @dojo/cli to learn how to get started.

The previous template application gave a you a nice introduction to basic widgets and how to display the widget in your app. It was fine as an introduction, but if you wanted to do a little more, like routing, you had to do a little more research. Not anymore! The new template application comes with routing out of the box so you can quickly get up and running with a feature that you will probably end up using at some point in a larger application.

Unfortunately, the latest template app is not on code sandbox, most likely due to the routing not working correctly in that environment, at least not the last time I tried.

Here is what the template application looks like.

I have put up the untouched source for the template application on github.

Now let’s take a look at what you get with the new template app.

I’ll do a more detailed post on routing in the future, but you can read more details in the Dojo documentation. The key here is that each view for a route is defined by an Outlet. An Outlet is just a wrapper for widgets that will be displayed in that routes view.

// src/App.ts
import WidgetBase from "@dojo/framework/widget-core/WidgetBase";
import { v, w } from "@dojo/framework/widget-core/d";
import Outlet from "@dojo/framework/routing/Outlet";

import Menu from "./widgets/Menu";
import Home from "./widgets/Home";
import About from "./widgets/About";
import Profile from "./widgets/Profile";

import * as css from "./App.m.css";

export default class App extends WidgetBase {
  protected render() {
    return v("div", { classes: [css.root] }, [
      w(Menu, {}),
      v("div", [
        w(Outlet, {
          key: "home", id: "home", renderer: () => w(Home, {})
        }),
        w(Outlet, {
          key: "about", id: "about", renderer: () => w(About, {})
        }),
        w(Outlet, {
          key: "profile",
          id: "profile",
          renderer: () => w(Profile, {
            username: "Dojo User"
          })
        })
      ])
    ]);
  }
}

Ok, so let’s break this down a little bit. The w is a function to render widgets and v will create virtual dom nodes. You can see that in this case, what is happening is there is a top level menu, with a div underneath. In this div is where each Outlet is defined, with an id, key (optional), and what to display in the render method.

I won’t go in to detail on each view. They are fairly standard widgets, but let’s take a look at the routing part. The routes are defined in a simple object.

// src/routes.ts
export default [
  {
    path: "home",
    outlet: "home",
    defaultRoute: true
  },
  {
    path: "about",
    outlet: "about"
  },
  {
    path: "profile",
    outlet: "profile"
  }
];

Each route has a path, with the name of the outlet id, which coincides with the id of the outlet defined in the previous snippet. Super simple and straight forward. You can also see that the home route is defined as the defaultRoute.

Here is how the whole thing is put together.

// src/main.ts
import renderer from '@dojo/framework/widget-core/vdom';
import Registry from '@dojo/framework/widget-core/Registry';
import { w } from '@dojo/framework/widget-core/d';
import { registerRouterInjector } from '@dojo/framework/routing/RouterInjector';
import {
  registerThemeInjector
} from '@dojo/framework/widget-core/mixins/Themed';
import dojo from '@dojo/themes/dojo';
import '@dojo/themes/dojo/index.css';

import routes from './routes';
import App from './App';

const registry = new Registry();
registerRouterInjector(routes, registry);
registerThemeInjector(dojo, registry);

const r = renderer(() => w(App, {}));
r.mount({ registry });

I’ll go into more detail in the future, but you register your route with the Registry, which is a way that you can do more configuration with your widgets beyond just display them on the page. You even get a taste of working with themes via the ThemeInjector.

Once your routes are registered, you can then mount the application with the registry. If I were to do anything different here, it would probably be to do all the Registry work in a separate module, but that is just a preference.

I am really glad to see the new dojo cli template app giving users a solid start with routing and an introduction to the registry, which in my opinion are key components of building scalable applications.

Now, why is routing important in progressive web apps? It allows you to lazy load parts of your application until you need them. For example, in the template application some users may never click on the profile page, so why should your application load the files for that page unnecessarily . You can see what I mean in this animated image.

Here, you can see that the files for the pages are not loaded until I click on them. This is code splitting, something Dojo 1 was fantastic at and that the new Dojo takes advantage of webpack under the hood in their build tools to handle as well.