3

I would really like to take advantage of Durandal's buildNavigationModel() method and bind my UI nav to the router.navigationModel.

But in my case, I am essentially wanting three menu items, which all use the same underlying view and module, but vary only by parameter.

    // . . . 

    activate: function () {
        var standardRoutes = [
            { route: 'home', title: 'KPI Home', iconClass: "glyphicon-home", moduleId: 'viewmodels/kpihome', nav: true },
            { route: 'summary(/:category)', title: 'Quotes', iconClass: "glyphicon-home", moduleId: 'viewmodels/summary', hash: "#summary/quotes", nav: true },
            { route: 'summary(/:category)', title: 'Pricing', iconClass: "glyphicon-home", moduleId: 'viewmodels/summary', hash: "#summary/pricing", nav: true },
            { route: 'summary(/:category)', title: 'Sales', iconClass: "glyphicon-home", moduleId: 'viewmodels/summary', hash: "#summary/sales", nav: true }
        ];

        router
            .map(standardRoutes)
            .buildNavigationModel();

        return router.activate();
    }

So while the hash is different, and I can pick up the category passed in to the summary module's activate method, when I click on either of the other routes, the first matching route isActive is flagging true. In other words, isActive works off the route pattern rather than an exact hash comparison.

Would anyone be able to recommend an alternate/best practice approach to this, where I could re-use route patterns and modules and still have a working nav?

My current solution would be to create the route only once and build my own nav model.

Stephen James
  • 1,277
  • 9
  • 17
  • Have you looked into creating a child router for your summary module? – Brett Jun 16 '14 at 17:40
  • I looked into it but couldn't find a way to achieve what I was looking for. Ultimately I would like hierarchical routes with the sub routes always loaded, so that I can present a hierarchical nav. This question solves it and they recommend not using child routers. http://stackoverflow.com/questions/19483651/durandal-2-0-child-routers-intended-for-nested-menus. Admittedly, I am not hugely experienced with Durandal so am just going on other's recommendations here. I will edit my question to show my current implementation, which uses a custom navigation model. – Stephen James Jun 17 '14 at 09:45

1 Answers1

1

After a bit of digging I've found three potential solutions to this problem.

  1. Use child routers
  2. Use a custom identifier in the routes to describe child routes and parse these into the route table
  3. Leave routing to the route table and create a custom navigation model

I'll chat through my opinion on each below and how I got to a solution. The following post really helped me a lot Durandal 2.0 - Child routers intended for nested menus?

Use child routers

While this makes a lot of sense when you want to create child routes that exist in a view served up by the main router, my requirement is to have navigation visible at a shell level that contains all sub routes, always visible and loaded.

According to the article mentioned above

"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"

So I'm basing my decision on that (hopefully correct info) and for my case that looks like its not going to work.

Use a custom identifier in the routes to describe child routes and parse these into the route table

This works and is implemented neatly in the the article mentioned above. But does the route table have the same concerns as UI navigation? In some cases, sure it can share that, but in my case not, so I'm going with option 3, creating a custom nav model.

Creating a Custom Navigation Model

I'm needing to re-use some views for navigation items, to display the same summary and detail views, but for different categories and kpi's that I'll pass in by parameter.

From a route table perspective there are only three routes - the routes to a home view, a summary view and a detail view.

From a nav perspective there are n navigation items, depending on the number of categories and kpi's I want to display summary and detail views for. I'll effectively be putting links up for all the items I want to show.

So it makes sense that I build up the nav model independently of the route table.

utility\navigationModel.js

defines the navigation model and responds to hash changes to keep a record in the activeHash observable

define(["knockout", "utility/navigationItem"], function (ko, NavItem) {
    var NavigationModel = function () {
        this.navItems = ko.observableArray();
        this.activeHash = ko.observable();

        window.addEventListener("hashchange", this.onHashChange.bind(this), false);

        this.onHashChange();
    };

    NavigationModel.prototype.generateItemUid = function () {
        return "item" + (this.navItems().length + 1);
    };

    NavigationModel.prototype.onHashChange = function () {
        this.activeHash(window.location.hash);
    };

    NavigationModel.prototype.findItem = function (uid) {
        var i = 0,
            currentNavItem,
            findRecursive = function (uid, base) {
                var match = undefined,
                    i = 0,
                    childItems = base.navItems && base.navItems();

                if (base._uid && base._uid === uid) {
                    match = base;
                } else {
                    for (; childItems && i < childItems.length; i = i + 1) {
                        match = findRecursive(uid, childItems[i]);
                        if (match) {
                            break;
                        }
                    }
                }

                return match;
            };

        return findRecursive(uid, this);
    };

    NavigationModel.prototype.addNavigationItem = function (navItem) {
        var parent;

        if (navItem.parentUid) {
            parent = this.findItem(navItem.parentUid);
        } else {
            parent = this;
        }

        if (parent) {
            parent.navItems.push(new NavItem(this, navItem));
        }

        return this;
    };

    return NavigationModel;
});

utility\navigationItem.js

represents a navigation item, with nav specific properties like iconClass, sub nav items navItems and a computed to determine if it is an active nav isActive

define(["knockout"], function (ko) {
    var NavigationItem = function (model, navItem) {
        this._parentModel = model;

        this._uid = navItem.uid || model.generateItemUid();
        this.hash = navItem.hash;
        this.title = navItem.title;
        this.iconClass = navItem.iconClass;

        this.navItems = ko.observableArray();

        this.isActive = ko.computed(function () {
            return this._parentModel.activeHash() === this.hash;
        }, this);
    }

    return NavigationItem;
});

shell.js

defines standard routes for the route table and builds up the custom navigation. Implemented properly this would likely call a dataservice to lookup categories and kpi's for the nav model

define([
    'plugins/router', 
    'durandal/app', 
    'utility/navigationModel'
], function (router, app, NavigationModel) {

    var customNavigationModel = new NavigationModel(),

        activate = function () {
            // note : routes are required for Durandal to function, but for hierarchical navigation it was 
            // easier to develop a custom navigation model than to use the Durandal router's buildNavigationModel() method
            // so all routes below are "nav false".
            var standardRoutes = [
                { route: '', moduleId: 'viewmodels/kpihome', nav: false },
                { route: 'summary(/:category)', moduleId: 'viewmodels/summary', hash: "#summary/quotes", nav: false },
                { route: 'kpidetails(/:kpiName)', moduleId: 'viewmodels/kpidetails', hash: "#kpidetails/quotedGMPercentage", nav: false }
            ];

            router.map(standardRoutes);

            // Fixed items can be added to the Nav Model
            customNavigationModel
                .addNavigationItem({ title: "KPI Home", hash: "", iconClass: "glyphicon-home" });

            // items by category could be looked up in a database
            customNavigationModel
                .addNavigationItem({ uid: "quotes", title: "Quotes", hash: "#summary/quotes", iconClass: "glyphicon-home" })
                .addNavigationItem({ uid: "sales", title: "Sales", hash: "#summary/sales", iconClass: "glyphicon-home" });

            // and each category's measures/KPIs could also be looked up in a database and added
            customNavigationModel
                .addNavigationItem({ parentUid: "quotes", title: "1. Quoted Price", iconClass: "glyphicon-stats", hash: "#kpidetails/quotedPrice" })
                .addNavigationItem({ parentUid: "quotes", title: "2. Quoted GM%", iconClass: "glyphicon-stats", hash: "#kpidetails/quotedGMPercentage" });

            customNavigationModel
                .addNavigationItem({ parentUid: "sales", title: "1. Quoted Win Rate", iconClass: "glyphicon-stats", hash: "#kpidetails/quoteWinRate" })
                .addNavigationItem({ parentUid: "sales", title: "2. Tender Win Rate ", iconClass: "glyphicon-stats", hash: "#kpidetails/tenderWinRate" });

            return router.activate();
        };

    return {
        router: router,
        activate: activate,
        customNavigationModel: customNavigationModel
    };
});

And thats it, a fair amount of code, but once in place it seperates the route table and the navigation model fairly nicely. All that remains is binding it to the UI, which I use a widget to do because it can serve as a recursive template.

widgets\verticalNav\view.html

<ul class="nav nav-pills nav-stacked" data-bind="css: { 'nav-submenu' : settings.isSubMenu }, foreach: settings.navItems">
    <li  data-bind="css: { active: isActive() }">
        <a data-bind="attr: { href: hash }">
            <span class="glyphicon" data-bind="css: iconClass"></span>
            <span data-bind="html: title"></span>
        </a>
        
        <div data-bind="widget: {
                kind: 'verticalNav',
                navItems: navItems,
                isSubMenu: true
            }">
        </div>
    </li>
</ul>

I'm not suggesting this is the best way to do this, but if you want to seperate the concerns of a route table and a navigation model, its a potential solution :)

Community
  • 1
  • 1
Stephen James
  • 1,277
  • 9
  • 17