Using Bootstrap 4 with a multilevel dropdown nav menu

I have a multi level dropdown bootstrap 4 navbar. It works in a fashion but I have the problem in that if a page is selected from one of the multi levels rather than the immediate menu I loose the activate class.

Wondering if someone might guide me to how I can get activate on lower multi level options?

Here is the navmenu.html

<template>
<require from="./navmenu.scss"></require>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mainmenu mr-auto">
	<a class="navbar-brand" href="#/home">JobsLedger</a>
	<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
		<span class="navbar-toggler-icon"></span>
	</button>

	<div class="navbar-collapse collapse" id="navbarSupportedContent">
		<ul class="navbar-nav mr-auto">
			<li repeat.for="route of router.navigation">
				<a href.bind="route.href" if.bind="!route.settings.nav" class="menu-level-link ${route.isActive ? 'active' : ''}"><font-awesome-icon icon="${ route.settings.icon }"></font-awesome-icon> ${route.title}</a>
				
				<a href.bind="route.href" if.bind="route.settings.nav" class="menu-level-dropdown dropdown-toggle ${route.isActive ? 'active' : ''}" id="navbarDropdown" role="button" data-toggle="dropdown"  aria-haspopup="true" aria-expanded="false">
					<font-awesome-icon icon="${ route.settings.icon }"></font-awesome-icon>
					${route.title} 
				</a>

				<ul if.bind="route.settings.nav" class="dropdown-menu" aria-labelledby="navbarDropdown" role="button">
					<li repeat.for="menu of route.settings.nav" class="${menu.settings.divider ? 'divider' : 'dropdown-submenu '}">
						<a href.bind="menu.href" if.bind="!menu.settings.nav" class="dropdown-submenu-link">
							<font-awesome-icon icon="${ menu.settings.icon }"></font-awesome-icon>
							${menu.title}
						</a>
						<a href.bind="menu.href" if.bind="menu.settings.nav" class="dropdown-toggle" data-toggle="dropdown" role="button">
							<font-awesome-icon icon="${ menu.settings.icon }"></font-awesome-icon>
							${menu.title} <span class="caret-right"></span>
						</a>

						<ul if.bind="menu.settings.nav" class="dropdown-menu" aria-labelledby="navbarDropdown">
							<li repeat.for="subMenu of menu.settings.nav" class="${subMenu.settings.divider ? 'divider' : 'dropdown-submenu '}">
								<a href.bind="subMenu.href" if.bind="!subMenu.settings.nav" class="dropdown-submenu-link">
									<font-awesome-icon icon="${ subMenu.settings.icon }"></font-awesome-icon>
									${subMenu.title}
								</a>
								<a href.bind="subMenu.href" if.bind="subMenu.settings.nav" class="dropdown-toggle" data-toggle="dropdown" role="button">
									<font-awesome-icon icon="${ subMenu.settings.icon }"></font-awesome-icon>
									${subMenu.title} <span class="caret-right"></span>
								</a>

								<ul if.bind="subMenu.settings.nav" class="dropdown-menu" aria-labelledby="navbarDropdown">
									<li repeat.for="lowestSubMenu of subMenu.settings.nav" class="${lowestSubMenu.settings.divider ? 'divider' : 'dropdown-submenu '}">
										<a href.bind="lowestSubMenu.href" if.bind="!lowestSubMenu.settings.divider" class="dropdown-submenu-link">
											<font-awesome-icon icon="${ lowestSubMenu.settings.icon }"></font-awesome-icon>
											${lowestSubMenu.title}
										</a>
									</li>
								</ul>

							</li>

						</ul>

					</li>

				</ul>

			</li>

		</ul>


		<ul class="nav navbar-nav navbar-right mr-auto">
			<li repeat.for="row of routes" if.bind="row.settings.pos == 'right'" class="${ row.isActive ? 'link-active' : '' }">
				<a href.bind="row.href" if.bind="!row.settings.nav">${ row.title }</a>

				<a href.bind="row.href" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"
					 if.bind="row.settings.nav">
					${row.title}
					<span class="caret"></span>
				</a>

				<ul if.bind="row.settings.nav" class="dropdown-menu">
					<li repeat.for="menu of row.settings.nav">
						<a href.bind="menu.href">${menu.title}</a>
					</li>
				</ul>
			</li>
			<li><a>Welcome ${userName}</a></li>
			<li><a href="#" click.delegate="logout()">Log Out</a></li>
		</ul>
	</div>
</nav>

You can see that at the top level I use a ternary function

class=“menu-level-link ${route.isActive ? ‘active’ : ‘’}”

…and it works for those menu items that are directly under the menu option but should I select a second or third level option the menu item looses its active class tag…

No idea how to fix this… Hope someone might have a solution

Simon

So if I understand you correctly, what you’re looking for is a evaluation of whether the current route (row) is either active or recursively a route found in route.settings.nav is active.

This can be done with a simple value converter, something in the likes of (pseudo and untested):

export class ActiveRouteValueConverter {
   toView(route) {
     function evaluateRoute(route) {
        if (route.isActive) {
           return true;
        } else { 
          if (route.settings.nav) {
             return route.settings.nav.any(evaluateRoute);
          }
          else {
             return false;
          }
        }

        return evaluateRoute(route);
     }
   }
}

Next in your view you can test against the results of this using: class="menu-level-link ${route | activeRoute ? ‘active’ : ‘’}"

1 Like

I reckon this will work however I am having trouble wiring it up… I looked at the documentation for value converters and then implemented it as follows:

Created the file “activeRoute.ts” in the same folder as “navemenu.html”.

Copied the code above (I shall work on this more later) but added console.log(“Gets Here!!”) at the very top before it does anything to see if it gets called…

Added <require from="./activeRoute"></require> to the top of navmenu.ts

It doesnt render now with the error:

Uncaught (in promise) Error: Parser Error: Unconsumed token ? at column 20 in expression [route | activeRoute ? ‘active’ : ‘’]
at ParserImplementation.err (aurelia-binding.js:2857)
at ParserImplementation.parseBindingBehavior (aurelia-binding.js:2392)
at Parser.parse (aurelia-binding.js:2349)
at TemplatingBindingLanguage.parseInterpolation (aurelia-templating-binding.js:835)
at TemplatingBindingLanguage.inspectAttribute (aurelia-templating-binding.js:699)
at ViewCompiler._compileElement (aurelia-templating.js:2976)
at ViewCompiler._compileNode (aurelia-templating.js:2776)
at ViewCompiler._compileNode (aurelia-templating.js:2798)
at ViewCompiler.compile (aurelia-templating.js:2745)
at HtmlBehaviorResource.compile (aurelia-templating.js:4258)
err

I note that it did load the file activeRoute:

DEBUG [templating] importing resources for app/navmenu/navmenu.html 
(2) ["app/navmenu/navmenu.scss", "app/navmenu/activeRoute"]
     0: "app/navmenu/navmenu.scss"
     1: "app/navmenu/activeRoute"

Im not sure whats causing this… is it the fact the value converter is faulty or is something I have done.

1 Like

So it is loading the value converter… it fails on “?” in the ternary and I dont know why. . looked at the Aurelia Inspector at route and I see that it should work as “isActive” exists.

route:  NavModel
▶ isActive:  true
▶ title:  "scheduler"
▶ href:  "#/"
▶ relativeHref:  ""
▶ settings:  Object
▶ config:  Object
▶ router:  AppRouter
1 Like

I changed activeRoute to the following (as simple as I could make it) and I still get that same error…

export class ActiveRouteValueConverter {
toView(route) {
	console.log("gets here!!")
	return route.isActive;
  }
}

I would have thought this would work…

1 Like

My bad, the use of the value converter in my example is wrong. A value converter can convert between the bindingContext (viewmodel) and view. A valid use of the value converter would be something like this:
<a class="menu-level-link ${route | activeRoute:'active'}"> ${route.title}</a> "

and in the value converter:

toView(route, conditionalResult) {
  if (.....) // validate if route is active
    return conditionalResult;
  else 
    return null;
}
1 Like