dojo dragon main logo

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>
    );
});