- 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.ts
export 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
registry
to define arouter
injector 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