durandalhottoweldurandal-navigation

Durandal 2.0 - Child routers intended for nested menus?


I've building an app, and wanting to show a 2-tier menu, with both tiers always available. Durandal 2.0 introduced their new router, which supports 'child routers', which allow for easier deeplinking.

My question - Can I have my 'child' navigation routes permanently loaded (and a sub menu rendered when the parent is not active), or is the 'child router' design intended to lazy-load them to evaluate them once a deeplink is to be evaluated?

The Durandal examples show main navigation register a splat route, then when that view model is loaded/activated, the child router is registered.

E.g.: In the example provided with Durandal 2.0, the mian nev is registered in shell.js

router.map([
                { route: ['', 'home'], moduleId: 'hello/index', title: 'Validation test', nav: true },
                { route: 'knockout-samples*details',    moduleId: 'ko/index',               title: 'Knockout Samples',  nav: true, hash: '#knockout-samples' }
            ]).buildNavigationModel()
              .activate();

And the ko/index.js view model then registers the children (on activate())

var childRouter = router.createChildRouter()
        .makeRelative({
            moduleId:'ko',
            fromParent:true
        }).map([
            { route: '',                moduleId: 'helloWorld/index',       title: 'Hello World',           type: 'intro' },
            { route: 'helloWorld',      moduleId: 'helloWorld/index',       title: 'Hello World',           type: 'intro',      nav: true},
            { route: 'clickCounter',    moduleId: 'clickCounter/index',     title: 'Click Counter',         type: 'intro',      nav: true}
        ]).buildNavigationModel();

Whereas I'm looking to define my routes in one place, e.g.:

 var routes = [
   { route: ['', 'home'], 
        moduleId: 'hello/index', 
        title: 'Validation test', 
        nav: true },
   { route: 'knockout-samples*details',    
        moduleId: 'ko/index',
        moduleRootId: 'ko', // Custom property to make child routes easier              
        title: 'Knockout Samples',  
        nav: true, 
        hash: '#knockout-samples',
        childRoutes: [
            { route: '',                moduleId: 'helloWorld/index',       title: 'Hello World',           type: 'intro' },
            { route: 'helloWorld',      moduleId: 'helloWorld/index',       title: 'Hello World',           type: 'intro',      nav: true},
            { route: 'clickCounter',    moduleId: 'clickCounter/index',     title: 'Click Counter',         type: 'intro',      nav: true}
        ]}
   ])

And the part I'm struggling with is registering the child routes, and have them evaulate correctly when navigating to them. (Rendering the menu would come later...)

// Load routes              router.map(routes.navRoutes).buildNavigationModel().mapUnknownRoutes(function(instruction)
{
    logger.logError('No Route Found', instruction.fragment, 'main', true);
});

// Try load child routes...
$.each(routes.navRoutes, function(index, parentRoute) {
     if (!parentRoute.childRoutes) {
                return;
            }

            var childRouter = router.createChildRouter()
                .makeRelative({
                    moduleId: parentRoute.moduleRootId,
                    fromParent: true // also tried false + non-splat routes...
                }).map(parentRoute.childRoutes).buildNavigationModel();
        });

I've encountered various errors in getting this nav to render. Mostly internal router.js errors when trying to calculate the hash (either from a non-activated parent, or the child) - so all hashes have been defined.

Once I've got it to map the navigation, the child routes don't seem to be accessible - they just load the main splat page without error.

So I'm wondering if I'm going about this the right way at all? (Registering all child routes up front, with the goal of being about to render a 2-tier menu).

I'm thinking the fallback is using something like the Durandal 1.2 answer about subrouting, using a combination of flat routing registration, custom 'isSameItem' function & computed observables to render the 2-tier navigation.


Solution

  • I do hope I can help You a bit.

    Can You have 'child' routers permanently loaded?

    As far I know You can't ( or rather there is not simple way to achieve that). Child routers are designed to provide sub-routing capability for particular view - You can think about them as a Durandal navigation inside Durandal view. If we check the example code on Durandal page we can easily see that child routing life-time is connected with given view. What is more if we check the code of function creating child routes we will see that it creates new router and only store reference to parent router - the parent router ( in most cases main router) does not have references to its childs

    router.createChildRouter = function() {
                var childRouter = createRouter();
                childRouter.parent = router;
                return childRouter;
            }; 
    

    What can be done to achieve multilevel routing in main router?

    I had similar problem in the past but with old version of Durandal. For this problem I had started from scratch You gave and modifed it a bit - I get rid of splat route as its intend to use with child routes and my solution will not use them.

    var routes = [
                    {
                        route: ['', 'home'],
                        moduleId: 'viewmodels/home',
                        title: 'Validation test',
                        nav: true
                    },
                    {
                        route: 'knockout-samples',
                        moduleId: 'viewmodels/ko/index',
                        moduleRootId: 'viewmodels/ko', // Custom property to make child routes easier              
                        title: 'Knockout Samples',
                        nav: true,
                        hash: '#knockout-samples',
                        childRoutes: [
                            { route: 'simpleList', moduleId: 'simpleList', title: 'SimpleList',  nav: true, hash : 'simpleList' },
                            { route: 'clickCounter', moduleId: 'clickCounter', title: 'Click Counter', nav: true, hash : 'clickCounter' }
                        ]
                    }
                ];
    

    Next step is converting this user-friendly definition to route table which can be easily register in main router.

    $.each(routes, function(index, route) {
                    if (route.childRoutes === undefined)
                        return
                    $.each(route.childRoutes, function(index, childRoute) {
                        childRoute.route = route.route + '/' + childRoute.route;
                        childRoute.moduleId = route.moduleRootId + '/' + childRoute.moduleId;
                        childRoute.title = route.title + '/' + childRoute.title;
                        childRoute.hash = route.hash + '/' + childRoute.hash;
                        childRoute.parent = route.moduleRootId;
                    });
                    routes = routes.concat(route.childRoutes);
                });
    

    Last step is standard registering router and activating it.

    return router.map(routes)
                    .buildNavigationModel()
                    .activate();
    

    How to render register routing so multilevel layout is preserved?

    My code works with Bootstrap 2.3.0 and render multilevel menu as dropdown button. When route has no child routes ( so is just simple route) and has no parent ( its 1st level navigation) is rendered as simple button. If route has child routes its rendered as dropdown button and child routes are added to dropdown list.

    <div class="btn-group" data-bind="foreach: router.navigationModel">
                <!-- ko if: $data.childRoutes === undefined && $data.parent === undefined -->
                    <a data-bind="css: { active: isActive }, attr: { href: hash }, text: title" class="btn btn-info" href="#"/>
                <!-- /ko -->
                <!-- ko if: $data.childRoutes !== undefined -->
                <div class="btn-group btn-info">
                    <a data-bind="css: { active: isActive }, attr: { href: hash }, text: title" class="btn btn-info" href="#"/>
                    <button class="btn btn-info dropdown-toggle" data-toggle="dropdown">
                        <span class="caret"/>
                    </button>
                    <ul class="dropdown-menu" data-bind="foreach: childRoutes">
                        <a data-bind="css: { active: isActive }, attr: { href: hash }, text: title" class="btn btn-info" href="#"/>
                    </ul>
                </div>
                <!-- /ko -->
            </div>
    

    The styles probably need to be bit polished but overall logic is done.