- Getting Started
- Dojo local installation
- Your first Dojo application
- Components of a Dojo application
- Creating widgets
- Responding to events
- Form widgets
- Deploying to production
- Advanced
- Theming
- Web Animations
- State management
- Dojo Stores
- Form validation
- Registry
- Routing
- Setting up a development environment
- Data-driven widgets
Tutorials
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
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 againstoutlet- a unique identifier associated with a routedefaultParams- default parameters are used as a fallback when parameters do not exist in the current routedefaultRoute- a default route to be used if there is no other matching routechildren- 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.
main.ts.import { Registry } from '@dojo/framework/widget-core/Registry';
import { registerRouterInjector } from '@dojo/framework/routing/RouterInjector';
import routes from './routes';
routes.tsexport default [
{
path: 'directory',
outlet: 'directory',
children: [
{
path: '{filter}',
outlet: 'filter'
}
]
},
{
path: 'new-worker',
outlet: 'new-worker'
},
{
path: '/',
outlet: 'home',
defaultRoute: true
}
];
Explanations for the route configuration in the above code block are explained earlier in this step.
registry.const registry = new Registry();
registerRouterInjector(routes, registry);
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
registryto define arouterinjector against - An object which specifies the history manager to be used
The utility returns the router instance if required.
To initialize the routing, pass the registry to the renderer’s mount function.
r.mount({ domNode: document.querySelector('my-app') as HTMLElement, registry });
Next, we will create outlets to control when our widgets are displayed.
Routing Outlets
Outlet widget.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 matchpartial: The route is a match but not exacterror: The route is an error match
isError: Helper function that returns true when the match type is errorisExact: 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.
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.
w(Outlet, {
id: 'outlet-name',
renderer: (matchDetails: MatchDetails) => {
if (matchDetails.isExact()) {
return w(MyIndexComponent, {});
} else if (matchDetails.isError()) {
return w(MyErrorComponent, {});
}
return w(MyComponent, {});
}
});
Outlet widget and MatchDetails interface.import { Outlet } from '@dojo/framework/routing/Outlet';
import { MatchDetails } from '@dojo/framework/routing/interfaces';
Banner with an Outlet that returns Banner.w(Outlet, { id: 'home', renderer: () => {
return w(Banner, {});
}}),
WorkerForm with an Outlet that returns WorkerForm.w(Outlet, { id: 'new-worker', renderer: () => {
return w(WorkerForm, {
formData: this._newWorker,
onFormInput: this._onFormInput,
onFormSave: this._addWorker
});
}}),
The filter outlet use the match details information to determine if the filter param should be passed to the WorkerContainer widget.
WorkerContainer with an Outlet that returns WorkerContainer. w(Outlet, { id: 'filter', renderer: (matchDetails: MatchDetails) => {
if (matchDetails.isExact()) {
return w(WorkerContainer, {
workerData: this._workerData,
filter: matchDetails.params.filter
});
}
return w(WorkerContainer, { workerData: this._workerData });
}})
])
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.
Adding Links
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.
w(Link, { to: 'outlet-name', params: { paramName: 'value' } });
Link import in App.ts.import { Link } from '@dojo/framework/routing/Link';
render function in App.ts.protected render() {
return v('div', [
v('div', { classes: this.theme(css.root) }, [
v('div', { classes: this.theme(css.container) }, [
v('h1', { classes: this.theme(css.title) }, [ 'Biz-E-Bodies' ]),
v('div', { classes: this.theme(css.links) }, [
w(Link, { key: 'home', to: 'home', classes: this.theme(css.link) }, [ 'Home' ]),
w(Link, { key: 'directory', to: 'directory', classes: this.theme(css.link) }, [ 'Worker Directory' ]),
w(Link, { key: 'newWorker', to: 'new-worker', classes: this.theme(css.link) }, [ 'New Worker' ])
])
])
]),
v('div', { classes: this.theme(css.main) }, [
w(Outlet, { id: 'home', renderer: () => {
return w(Banner, {});
}}),
w(Outlet, { id: 'new-worker', renderer: () => {
return w(WorkerForm, {
});
}}),
w(Outlet, { id: 'filter', renderer: (matchDetails: MatchDetails) => {
if (matchDetails.isExact()) {
return w(WorkerContainer, {
workerData: this._workerData,
filter: matchDetails.params.filter
});
}
return w(WorkerContainer, { workerData: this._workerData });
}})
])
]);
}
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
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}.
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.
WidgetContainerProperties interface in WorkerContainer.ts.export interface WorkerContainerProperties {
workerData: WorkerProperties[];
filter?: string;
}
Link importimport { Link } from '@dojo/framework/routing/Link';
private _createFilterLinks() {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const links = [];
for (let i = 0; i < alphabet.length; i++) {
const char = alphabet.charAt(i);
links.push(
v('span', { classes: this.theme(css.links) }, [
w(Link, { key: char, to: 'filter', params: { filter: char }}, [ char ])
])
);
}
return links;
}
protected render() {
const {
workerData = [],
filter
} = this.properties;
const workers = workerData.filter((worker) => {
if (filter) {
return worker.lastName && worker.lastName.charAt(0).toUpperCase() === filter;
}
return true;
}).map((worker, i) => w(Worker, {
key: `worker-${i}`,
...worker
}));
return v('div', {}, [
v('h1', { classes: this.theme(css.title) }, [ 'Worker Directory' ]),
v('div', { classes: this.theme(css.filters) }, this._createFilterLinks()),
v('div', { classes: this.theme(css.container) }, workers)
]);
}
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.
- Getting Started
- Dojo local installation
- Your first Dojo application
- Components of a Dojo application
- Creating widgets
- Responding to events
- Form widgets
- Deploying to production
- Advanced
- Theming
- Web Animations
- State management
- Dojo Stores
- Form validation
- Registry
- Routing
- Setting up a development environment
- Data-driven widgets