it:ad:durandal.js:howto:navigation_dynamic_menu_bars

IT:AD:Durandal.JS:HowTo:Navigation/Dynamic Menu Bars

See: IT:AD:Durandal.JS:HowTo:Navigation where we covered that.

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)?

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 settings object of each routes – to contain role names, etc.
  • add a new observable property to the router, that in turn invokes the visibleRoutes array, but filters it according to the settings properties.
  • point the menu to not the visible array – but the new observableArray property.

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.

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

  • /home/skysigal/public_html/data/pages/it/ad/durandal.js/howto/navigation_dynamic_menu_bars.txt
  • Last modified: 2023/11/04 01:42
  • by 127.0.0.1