Application routing

Overview

@dojo/framework/routing is a powerful set of tools to support declarative routing using a specialized widget that accepts a render property called an Outlet and a widget that creates links with a href generated from an outlet id.

In this tutorial, we will start with a basic application with no routing. We will use Dojo’s declarative routing to configure some routes, use the Outlet widget to define the view for each route and use the Link widget to create links for the application’s outlets.

Prerequisites

You can open the tutorial on codesandbox.io or download the demo project and run npm install to get started.

The @dojo/cli command line tool should be installed globally. Refer to the Dojo local installation article for more information.

You also need to be familiar with TypeScript as Dojo uses it extensively.

Configuring the router

Create the routing configuration

All application routes needs to be to configured when creating the router instance, otherwise entering a route will not trigger a transition within the application. The RouteConfig object is an object consisting of various properties:

  • path - the URL path to match against
  • outlet - a unique identifier associated with a route
  • defaultParams - default parameters are used as a fallback when parameters do not exist in the current route
  • defaultRoute - a default route to be used if there is no other matching route
  • children - nested route configurations which can represent a nested path within a parent route
    The route configuration should be static, i.e. not dynamically determined at runtime and defined as the default export of a module called routes.ts in the project’s src directory.

Application routing paths are assembled into a hierarchy based on the routing configuration. The children property of a parent route accepts an array of more route configurations.

Include the required imports in the file main.ts.

  1. import { Registry } from '@dojo/framework/widget-core/Registry';
  2. import { registerRouterInjector } from '@dojo/framework/routing/RouterInjector';
  3. import routes from './routes';

Define the routing configuration in routes.ts

  1. export default [
  2. {
  3. path: 'directory',
  4. outlet: 'directory',
  5. children: [
  6. {
  7. path: '{filter}',
  8. outlet: 'filter'
  9. }
  10. ]
  11. },
  12. {
  13. path: 'new-worker',
  14. outlet: 'new-worker'
  15. },
  16. {
  17. path: '/',
  18. outlet: 'home',
  19. defaultRoute: true
  20. }
  21. ];

Explanations for the route configuration in the above code block are explained earlier in this step.

Now, register the router in a registry.

  1. const registry = new Registry();
  2. registerRouterInjector(routes, registry);

History Managers
The default history manager uses hash-based (fragment style) URLs. To use one of the other provided history managers pass it as the HistoryManager in the third argument of registerRouterInjector.

The registerRouterInjector helper utility used in the code above is provided by @dojo/framework/routing, and can be used to create a routing instance. The utility accepts:

  • The application’s routing configuration
  • A registry to define a router injector against
  • An object which specifies the history manager to be used
    The utility returns the router instance if required.

Initialize the routing

To initialize the routing, pass the registry to the renderer’s mount function.

  1. r.mount({ domNode: document.querySelector('my-app') as HTMLElement, registry });

Next, we will create outlets to control when our widgets are displayed.

Routing Outlets

Wrap the application widgets with the Outlet widget.

Reminder
The path that is associated to an outlet name is defined by the routing configuration from the first section of this tutorial.

The Outlet widget accepts two properties, id and renderer. The id is the outlet from the routing configuration and the renderer is a function that returns widgets and nodes to display using v() and w().

The renderer function receives a MatchDetails object that provides information about the route match.

  • router: The router instance, which can be used to generate links.
  • queryParams: An object that contains any query params for the route.
  • params: An object that contains any path params for the route.
  • type: The type of match:
    • index: An exact match
    • partial: The route is a match but not exact
    • error: The route is an error match
  • isError: Helper function that returns true when the match type is error
  • isExact: Helper function that returns true when the match type is exact
    Consider an outlet configured for a path of about, the widget that it returns from the renderer will render for a selected route about (described as an index match). The widget will also display for any route that the outlet’s path partially matches, for example, about/company or about/company/team.

Warning: Make sure functions used in render property are bound!
Functions passed to v() and w() in a render property need to already have been bound to the correct context. This is because the render property is actually executed and returned from the Outlet and therefore will be bound to the context of the Outlet and not your widget.

Simply returning the widget or nodes that need to be displayed when an outlet has matched is usually all that is required, however there are scenarios where it is necessary to explicitly define a widget for an index or error match. This is where matchDetails is beneficial. By using the information from matchDetails, we can create simple logic to determine which widget to render for each scenario.

  1. w(Outlet, {
  2. id: 'outlet-name',
  3. renderer: (matchDetails: MatchDetails) => {
  4. if (matchDetails.isExact()) {
  5. return w(MyIndexComponent, {});
  6. } else if (matchDetails.isError()) {
  7. return w(MyErrorComponent, {});
  8. }
  9. return w(MyComponent, {});
  10. }
  11. });

Import the Outlet widget and MatchDetails interface.

  1. import { Outlet } from '@dojo/framework/routing/Outlet';
  2. import { MatchDetails } from '@dojo/framework/routing/interfaces';

Replace Banner with an Outlet that returns Banner.

  1. w(Outlet, { id: 'home', renderer: () => {
  2. return w(Banner, {});
  3. }}),

Replace WorkerForm with an Outlet that returns WorkerForm.

  1. w(Outlet, { id: 'new-worker', renderer: () => {
  2. return w(WorkerForm, {
  3. formData: this._newWorker,
  4. onFormInput: this._onFormInput,
  5. onFormSave: this._addWorker
  6. });
  7. }}),

The filter outlet use the match details information to determine if the filter param should be passed to the WorkerContainer widget.

Replace WorkerContainer with an Outlet that returns WorkerContainer.

  1. w(Outlet, { id: 'filter', renderer: (matchDetails: MatchDetails) => {
  2. if (matchDetails.isExact()) {
  3. return w(WorkerContainer, {
  4. workerData: this._workerData,
  5. filter: matchDetails.params.filter
  6. });
  7. }
  8. return w(WorkerContainer, { workerData: this._workerData });
  9. }})
  10. ])

Running the application now should display the Banner.ts by default, but also enable routing to the other widgets using the /directory and /new-worker routes.

Next, we will add a side menu with links for the created outlets.

Add a sidebar menu to the application.

In this section we will be using the Link widget, provided by @dojo/framework/routing, to create link elements with an href attribute for an outlet name. A label for the Link can be passed as children and parameter values for the outlet can be passed to a Link widget using the params property.

  1. w(Link, { to: 'outlet-name', params: { paramName: 'value' } });

Add the Link import in App.ts.

  1. import { Link } from '@dojo/framework/routing/Link';

Replace the render function in App.ts.

  1. protected render() {
  2. return v('div', [
  3. v('div', { classes: this.theme(css.root) }, [
  4. v('div', { classes: this.theme(css.container) }, [
  5. v('h1', { classes: this.theme(css.title) }, [ 'Biz-E-Bodies' ]),
  6. v('div', { classes: this.theme(css.links) }, [
  7. w(Link, { key: 'home', to: 'home', classes: this.theme(css.link) }, [ 'Home' ]),
  8. w(Link, { key: 'directory', to: 'directory', classes: this.theme(css.link) }, [ 'Worker Directory' ]),
  9. w(Link, { key: 'newWorker', to: 'new-worker', classes: this.theme(css.link) }, [ 'New Worker' ])
  10. ])
  11. ])
  12. ]),
  13. v('div', { classes: this.theme(css.main) }, [
  14. w(Outlet, { id: 'home', renderer: () => {
  15. return w(Banner, {});
  16. }}),
  17. w(Outlet, { id: 'new-worker', renderer: () => {
  18. return w(WorkerForm, {
  19. });
  20. }}),
  21. w(Outlet, { id: 'filter', renderer: (matchDetails: MatchDetails) => {
  22. if (matchDetails.isExact()) {
  23. return w(WorkerContainer, {
  24. workerData: this._workerData,
  25. filter: matchDetails.params.filter
  26. });
  27. }
  28. return w(WorkerContainer, { workerData: this._workerData });
  29. }})
  30. ])
  31. ]);
  32. }

The updated block of code continues to render the banner, worker form and worker container widgets, and additionally renders three Link widgets:

  • A link to the home
  • A link to the worker directory
  • A link to the new worker form
    Now, the links in the side menu can be used to navigate around the application!

Dynamic Outlet

Add filters to the WorkerContainer.ts widget and create an outlet.

Finally, we are going to enhance the WorkerContainer.ts with a filter on the workers’ last name. To do this we need to use the filter outlet configured in the first section. The key difference for the filter outlet is that the path is using a placeholder that indicates a path parameter, {filter}.

URL matching in routes
The Dojo Routing documentation on GitHub further explains how outlets map to URLs.

This means a route with any value will match the filter as long as the previous path segments match, so for the filter outlet a route of directory/any-value-here would be considered a match.

Add the new property to the WidgetContainerProperties interface in WorkerContainer.ts.

  1. export interface WorkerContainerProperties {
  2. workerData: WorkerProperties[];
  3. filter?: string;
  4. }

Include the Link import

  1. import { Link } from '@dojo/framework/routing/Link';

Add a private function to generate the filter links

  1. private _createFilterLinks() {
  2. const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
  3. const links = [];
  4. for (let i = 0; i < alphabet.length; i++) {
  5. const char = alphabet.charAt(i);
  6. links.push(
  7. v('span', { classes: this.theme(css.links) }, [
  8. w(Link, { key: char, to: 'filter', params: { filter: char }}, [ char ])
  9. ])
  10. );
  11. }
  12. return links;
  13. }

Re-work the render function to filter using the new property

  1. protected render() {
  2. const {
  3. workerData = [],
  4. filter
  5. } = this.properties;
  6. const workers = workerData.filter((worker) => {
  7. if (filter) {
  8. return worker.lastName && worker.lastName.charAt(0).toUpperCase() === filter;
  9. }
  10. return true;
  11. }).map((worker, i) => w(Worker, {
  12. key: `worker-${i}`,
  13. ...worker
  14. }));
  15. return v('div', {}, [
  16. v('h1', { classes: this.theme(css.title) }, [ 'Worker Directory' ]),
  17. v('div', { classes: this.theme(css.filters) }, this._createFilterLinks()),
  18. v('div', { classes: this.theme(css.container) }, workers)
  19. ]);
  20. }

We have added a new property named filter to WorkerContainerProperties in WorkerContainer.ts, which will be used to filter the workers based on their last name. When used by a normal widget this would be determined by its parent and passed in like any normal property. However for this application we need the route param value to be passed as the filter property. To achieve this, we can add a mapping function callback which receives an object argument consisting of four properties:

Previously, the raw Dojo widgets were rendered. Now, Outlets (which are also widgets) are rendered instead. These outlets ‘wrap’ the original widgets and pass-through parameters to the wrapped widget, as you define them in the Outlet callback function.

Summary

Dojo routing is a declarative, non-intrusive, mechanism to add complicated route logic to a web application. Importantly, by using a specialized widget with a render property, the widgets for the routes should not need to be updated and can remain solely responsible for their existing view logic.

If you would like, you can open the completed demo application on codesandbox.io or alternatively download the project.