How to use dropdowns for Durandal navigation?
Asked Answered
D

2

7

I've just started working with Durandal and all the pieces are falling into place, and am using the Hot Towel template to speed things up.

One thing that stumps me is how to create a more complex hierarchical navigation system than a buttongroup. Here's what I want to end up with:

A B C
A1 B1 C1
A2 B2 C2

A, B, and C are the top-level menus that have no routes attached to them - they are simply placeholders. I will have views and viewmodels for A1, A2, B1, B2, C1, and C2 and need those hash tags to be active links.

My best idea right now is to append the parent menu into each route's url and have code in nav.html that dynamically adds each node into the appropriate parent based on parsing the url. To be fully dynamic, it would add both the parent nodes and child nodes dynamically.

        {
          url: 'A_A1',
          moduleId: 'viewmodels/A_A1',
          name: 'A1',
          visible: true
        }

I have done a lot of searching for hierarchical navigation examples with Durandal but haven't seen anything yet. Is there a best practice out there for expanding the navigation functionality beyond the simple one-dimensional list? Am I overlooking some functionality in the router that I'm not seeing that would let me do this without embedding hierarchy information into the view names?

EDIT: I just marked an answer as correct, even though I wasn't happy with either solution presented. When selecting a framework to abstract and separate logic, presentation, and control it seems silly to start merging these constructs again just to provide more than a basic navigation shell. I have shifted my development efforts to angularjs where things like this become far more intuitive and can keep the separation. Hopefully Durandal can move forward a little more in the near future and I'll definitely re-evaluate it for future projects.

Dockery answered 15/7, 2013 at 23:27 Comment(0)
E
6

You could hard-code them into your shell view, but if you didn't want to do that you can do it this -

In your view, make a non-working anchor with /# that does nothing, with a drop down of a-routes, and another with a drop-down of b routes.

        <div class="nav-collapse collapse main-nav">
            <div class="btn-group">
                <a class="btn" href="/#">
                    <span class="text">A Routes</span> </a>
                <button class="btn dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></button>
                    <ul class="dropdown-menu pull-right">
                        <!-- ko foreach: aRoutes -->
                        <li data-bind="css: { active: isActive }">
                            <a data-bind="attr: { href: hash }, html: name"></a>
                        </li>
                        <!-- /ko -->
                    </ul>
            </div>
            <div class="btn-group">
                <a class="btn" href="/#">
                    <span class="text">B Routes</span> </a>
                <button class="btn dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></button>
                    <ul class="dropdown-menu pull-right">
                        <!-- ko foreach: bRoutes -->
                        <li data-bind="css: { active: isActive }">
                            <a data-bind="attr: { href: hash }, html: name"></a>
                        </li>
                        <!-- /ko -->
                    </ul>
            </div>
      </div>

Make some computed observables for the routes in your shell view model

    var aRoutes = ko.computed(function () {
        return router.allRoutes().filter(function (r) {
            return r.settings.aroute;
        });
    });

    var bRoutes = ko.computed(function () {
        return router.allRoutes().filter(function (r) {
            return r.settings.broute;
        });
    });

and in your route definition -

 {
     url: 'a1',
     moduleId: 'viewmodels/a1',
     name: 'A1',
     visible: false,
     settings: {aroute: true}
 },

This sets all your routes to false, and then gives them another attribute of aroute that is set to true. The computed filters down to only routes with that setting set to true.

Execrative answered 16/7, 2013 at 1:44 Comment(2)
This is an excellent idea - tag the route with its parent. I'm going to work on this a little to make it more dynamic, and perhaps even as a Durandal widget.Dockery
Hope it helps you, I have been using this method for a while and it works really well and is dynamic at the same time. I just saw I didn't have the right indention on the b routes so just fixed that. Good luck.Execrative
M
3

I devised a general solution after having read this post. With this solution you can dynamically add any routes to a dropdown menu in the nav.

In main.js, where i specifiy my routes, I added a function for mapping the submenu object to the main nav route's settings object.

function mapSubNav(parentRouteInfo) {
    var subroutes = [];
    var length = arguments.length;
    for (var i = 0; i < length; i++) {
        subroutes.push(arguments[i]);
    }
    parentRouteInfo.settings.subroutes = subroutes;
}

var page = router.mapNav('page', null, 'Page'),
    sub1 = router.mapRoute('page/sub1', null, 'Sub1'),
    sub2 = router.mapRoute('page/sub2', null, 'Sub2');
mapSubNav(nav, sub1, sub2);

Explanation:

The router function mapNav returns a route definition which looks like this:

{
    url:'flickr', //you provided this
    name: 'Flickr', //derived
    moduleId: 'flickr', //derived
    caption: 'Flickr', //derived (uses to set the document title)
    settings: {}, //default,
    hash: '#/flickr', //calculated
    visible: true, //from calling mapNav instead of mapRoute
    isActive: ko.computed //only present on visible routes to track if they are active in the nav
}

The helper function, mapSubNav, will place a list of references to the routes you want to appear in the drop-down menu in the settings object. In this example, the result will be:

{
    url:'page',
    name: 'Page',
    moduleId: 'page',
    caption: 'Page',
    settings: { subroutes: [nav, sub1, sub2] },
    hash: '#/page',
    visible: true,
    isActive: ko.computed
}

I extended my shell viewmodel to look like this:

define(function (require) {
    var router = require('durandal/plugins/router');
    var app = require('durandal/app');

    var ViewModel = function () {
        var self = this;
        self.router = router;
        self.dataToggle = function (route) {
            return !!route.settings.subroutes ? 'dropdown' : '';
        };
        self.html = function (route) {
            return !!route.settings.subroutes ? route.name + ' <b class="caret"></b>' : route.name;
        };
        self.hash = function (route) {
            return !!route.settings.subroutes ? '#' : route.hash;
        };
        self.divider = function (route, parent) {
            system.log('Adding', route, 'to dropdown', 'Parent', parent);
            return route.hash === parent.hash;
        };
        self.activate = function () {
            return router.activate('welcome');
        }
    };

    return new ViewModel();
});

Note:

The extra functions in shell.js will decide what attributes that should be added to the DOM elements in the nav.


And finally, my I edited my shell view to look like this;

<div class="nav-collapse collapse">
    <ul class="nav navbar-nav" data-bind="foreach: router.visibleRoutes()">
        <li data-bind="css: { active: isActive, dropdown: !!settings.subroutes }">
            <a data-bind="css: { 'dropdown-toggle': !!settings.subroutes },
                          attr: { href: $root.hash($data), 'data-toggle': $root.dataToggle($data) },
                          html: $root.html($data)"></a>
            <ul data-bind="foreach: settings.subroutes" class="dropdown-menu">
                <li><a data-bind="attr: { href: hash },
                                  html: name"></a></li>
                <li data-bind="css: { divider: $root.divider($data, $parent) }"></li>
            </ul>
        </li>
    </ul>
</div>

Result:

The final result is a menu item with a dropdown toggle. The dropdown menu will contain a link to the parent and to one link each to the subroutes. Something like this:

---------------------------------------------
 Menu items... | Page v | More menu items...
---------------------------------------------
               | Page   |
               ----------
               | Sub 1  |
               | Sub 2  |
               ----------

jQuery doesn't let you map a route to a dropdown-toggle button, which is why I made the parent route keep a reference to itself in the list of subroutes There will still exist a link to the parent route in the nav.

Meekins answered 13/8, 2013 at 14:21 Comment(2)
What is different from the answer I provided?Execrative
@PWKad With my solution, once everything is set up, you can just continue to use mapSubNav in main.js, and submenus will automatically be added, with no need to edit any other files. I did not see how that could be achieved with your solution. Did I miss anything?Meekins

© 2022 - 2024 — McMap. All rights reserved.