IT:AD:Durandal.JS:HowTo:Navigation/Dynamic Menu Bars
Summary
Process
Routing Basics
See: IT:AD:Durandal.JS:HowTo:Navigation where we covered that.
Creating Menus from the VisibleRoutes Array
We've already seen that app/durandal/plugins/router.js offeres the following ko.observables:
allRoutes = ko.observableArray([]),
visibleRoutes = ko.observableArray([]),
ready = ko.observable(false),
isNavigating = ko.observable(false),
...
activeItem = viewModel.activator(),
activeRoute = ko.observable(),
With the above router ko.observable arrays, the default Durandal demo shows a menu bar (built using Bootstrap) that binds to the visibleRoutes ko.observableArray:
<callout icon="true" type="class="navbar-inner">
<a role="d...cant remember just now..." class="brand" data-bind="attr: { href: router.visibleRoutes()[0].hash }">
<i class="icon-home"></i>
</a>
<ul class="nav" data-bind="foreach: router.visibleRoutes" role="navigation">
<li data-bind="css: { active: isActive }" role="menuitem">
<a data-bind="attr: { href: hash }, html: name"></a>
</li>
</ul>
<div role="d...." class="loader pull-right" data-bind="css: { active: router.isNavigating }">
<i class="icon-spinner icon-2x icon-spin"></i">
</callout>
<form role="search" class="navbar-search pull-right" data-bind="submit:search">
<input type="text" class="search-query" placeholder="Search...">
</form>
</div>
Notice how the first menu button is an icon, and it's link is pointing to the first member of the `visibleRoute` (the `Welcome` page) (same target as the first iteration of the `visibleRoutes` array, below it)?
Better Control
That's all nice – but not yet really useful. Some issues with the above are:
- There's duplication between the first menuitem/icon and first
li(both going to same place). You might want to see one, but not both buttons. - Secondly, there's no authenticated access control – for showing routes available when signed in, versus not yet signed in.
- There's no role management (some links should be available only when the user is in in role xyz).
What we're going to have to do is:
- add some more custom values to the
settingsobject of each routes – to contain role names, etc. - add a new
observableproperty to the router, that in turn invokes thevisibleRoutesarray, but filters it according to thesettingsproperties. - point the menu to not the visible array – but the new
observableArrayproperty.
First, the routes – let's add two new custom settingsproperties: 'authenticated' and 'roles':
settings.routes = [
//Something visible to anybody even if not authenticated:
{ url: 'welcome', moduleId: 'viewmodels/welcome', name: 'Home', visible: false, settings: {authenticated:false} },
//Something that available to all authenticated
{ url: 'students/search', moduleId: 'viewmodels/students/studentSearch', name: 'Search', visible: true, settings: {authenticated:true, roles: []} },
// Something available to all authenticated users, restricted to special roles:
{ url: 'admin', moduleId: 'viewmodels/admin', name: 'Admin', visible: true, settings: {authenticated:true, roles: ['Special']} },
{ url: 'flickr', moduleId: 'viewmodels/flickr', name: 'flickr', visible: true, settings: {'one', authenticated:false, roles: []} },
];
The current menu view is bound to the menu's viewmodel's router property:
data-bind="foreach: router.visibleRoutes"
We're going to change that, to point to a new observable that we haven't written yet:
data-bind="foreach: router.visibleValidRoutes"
And then, in our view model, we're going to attach a new observable to the router, so that it is available in this menu – and later anywhere else that needs it (as the router is a singleton). The property has to be an observable – so that the menu can be dynamically updated if the user signs in or out – and it has to update itself if any new routes are registered.
I'm going to suggest wrapping the already available router.allRoutes() – and filter it for the following (is visible, and is open to everyone or the user is authenticated, and a member of any mentioned roles:
router.visibleValidRoutes = ko.computed(function() {
var routes =
ko.utils.arrayFilter(
router.allRoutes(),
function(routeInfo) {
if (!routeInfo.visible){return false;}
var routeInfoSettings = routeInfo.settings;
//Ensure routeInfo does not need to be authenticated
if (!routeInfoSettings.authenticated){return true;}
//or user is in role
if (user.isAuthenticated()){
//If no roles defined, considered open to authorised users:
if ((!routeInfoSettings.roles)||(routeInfoSettings.roles.length==0)) {return true;}
//iterate through requested roles, looking for presence in user's defined roles.
var userRoles = user.roles();
for (var i=0;i<routeInfoSettings.roles.length;i++){
var x = routeInfoSettings.roles[i];
var r = $.inArray(x, userRoles);
if (r>-1){
return true;
}
}
}
return false;
}
);
//Hack: isActive property was attached in routes.js to original observable array...
//but get's lost when rewrapped by this filter...so have to hack/reattach to each element
ko.utils.arrayForEach(
routes,
function(route){
route.isActive = ko.computed(
function () {
return router.ready() && router.activeItem() && router.activeItem().__moduleId__ == route.moduleId;
});
}
);
return routes;
}
,router);
Note:
* Yest, I'm using a singleton user that injected into the menu viewmodel. It' pretty simple:
define (
function(){
var model = function(){
this.roles = ko.observable(null);
this.imageUrl = ko.observable(null);
this.emailAddress = ko.observable(null);
this.displayName = ko.observable(null);
this.userName = ko.observable(null);
this.isAuthenticated = ko.observable(false);
this.clear();
}
model.prototype.clear=function(){
this.roles(null);
this.imageUrl();
this.emailAddress(null);
this.displayName(null);
this.userName(null);
this.isAuthenticated(false);
}
return new model();
}
);
Tada…done.
The menu will now update automatically when the user signs in or out, and show only menu items relevant to the user.
Menus != Security
Just because a user can't see a link doesn't mean that they can't type the link in, and get to the page anyway.
That's not security.
See: Security