Outlets
An outlet represents a visual location of an application that renderers different content depending on which route has been matched. Using outlets reduces boilerplate required compared to using routes, multiple routes can be associated to the same outlet to more naturally and accurately structure the application output.
Consider a typical application layout which includes a left side menu and a main content view that depending on the route has a right hand side bar:
-------------------------------------------------------------------
| | | |
| | | |
| | | |
| | | |
| | | |
| menu | main | side-menu |
| | | |
| | | |
| | | |
| | | |
| | | |
-------------------------------------------------------------------
The route configuration below specifies all the main pages to the main content outlet, but the widget
to a side-menu
outlet. This enables building an application that constantly renders the main content depending on route, but also include a right hand side menu for all children routes of the widget
route.
const routes = [
{
id: 'landing',
path: '/',
outlet: 'main',
defaultRoute: true
},
{
id: 'widget',
path: 'widget/{widget}',
outlet: 'side-menu',
children: [
{
id: 'tests',
path: 'tests',
outlet: 'main'
},
{
id: 'overview',
path: 'overview',
outlet: 'main'
},
{
id: 'example'
path: 'example/{example}',
outlet: 'main'
}
]
}
];
In the routing configuration above, there are two outlets defined, main
and side-menu
, and a simplified application layout using outlets is shown below. By default the Outlet
will render any of the keys that equal a route id that has been matched for the outlet, in this case main
. If a function is passed to the Outlet
then it will render whenever any route is matched for the outlet specified.
import { create, tsx } from '@dojo/framework/core/vdom';
import Outlet from '@dojo/framework/routing/Outlet';
import Menu from './Menu';
import SideMenu from './SideMenu';
import Landing from './Landing';
import Tests from './Tests';
import Example from './Example';
const factory = create();
const App = factory(function App() {
return (
<div>
<Menu />
<main>
<div>
<Outlet id="main">
{{
landing: <Landing />,
tests: <Tests />,
example: ({ params: { example }}) => <Example example={example}/>,
overview: <Example example="overview"/>
}}
</Outlet>
</div>
<div>
<Outlet id="side-menu">
{({ params: { widget }}) => <SideMenu widget={widget}>}
</Outlet>
</div>
</main>
</div>
);
});
The node structure of the App
looks good and succinctly represents the actual visual output for the user with minimal duplication, there still is a need to duplicate the usage of the Example
widget across to different routes. This can be solved by using the matcher
property to override the default route matching rules. The matcher
receives the defaultMatches
and a matchDetailsMap
in order to make custom matching decisions. In the final example below the usage of Example
has been combined into a new key
, details
that does not exist as a route. This will never match for the outlet unless we override the default matches to set it to true when either the example
or overview
route has matched. Finally in the details
renderer the example property has been defaulted to overview
to maintain the same behavior as before.
import { create, tsx } from '@dojo/framework/core/vdom';
import Outlet from '@dojo/framework/routing/Outlet';
import Menu from './Menu';
import SideMenu from './SideMenu';
import Landing from './Landing';
import Tests from './Tests';
import Example from './Example';
const factory = create();
const App = factory(function App() {
return (
<div>
<Menu />
<main>
<div>
<Outlet id="main" matcher={(defaultMatches, matchDetailsMap) => {
defaultMatches.details = matchDetailsMap.has('example') || matchDetailsMap.has('overview');
return defaultMatches;
}}>
{{
landing: <Landing />,
tests: <Tests />,
details: ({ params: { example = "overview" }}) => <Example example={example}/>,
}}
</Outlet>
</div>
<div>
<Outlet id="side-menu">
{({ params: { widget }}) => <SideMenu widget={widget}>}
</Outlet>
</div>
</main>
</div>
);
});