Building a Dynamic Application Menu with Durandal.js, Knockout, and Bootstrap (Pt. 2)

Categories: JavaScript

Tags: Durandal, Knockoutjs, KoLite

In the previous post, I laid out a design for creating a dynamic menu system, specifically the object model that will be used in data binding.  In this post, we’ll look at the Knockout bindings and HTML structure to render the menus.

This will be pretty straightforward as far as taking our object model and applying markup to it, however, the one complicated part is dealing with sub-menus.  Because menu items can themselves contain other menu items, we need the ability to render a menu within a menu and so on.  We can do this with Durandal’s compose binding handler for KO.  It allows recursive composition that is perfect for hierarchical things like menus.

Here’s the contents of views/menu.html which is the view component for a single Menu rendering:

<ul class="dropdown-menu" data-bind="foreach: items">
    <!-- ko if: $data.text !== undefined -->
    <li data-bind="css: { disabled: !command.canExecute(), 'dropdown-submenu': hasSubMenu }">
        <!-- ko ifnot: $data.hasSubMenu -->
        <a href="#" tabindex="-1" data-bind="command: command">
            <span data-bind="text: text"></span>
        </a>
        <!-- /ko -->
        <!-- ko if: $data.hasSubMenu -->
        <a tabindex="-1" data-bind="text: text"></a>
            <!-- ko compose: { view: 'views/menu.html', model: $data } -->
            <!-- /ko -->
        <!-- /ko -->
    </li>
    <!-- /ko -->
    <!-- ko if: $data.divider !== undefined -->
    <li class="divider"></li>
    <!-- /ko -->
</ul>

 

This markup is a bit complicated so let’s go through it:

  1. In Bootstrap, applying the dropdown-menu class to a <ul> will style it as a drop down menu container.  The data-bind here is also set to loop over each item in the Menu object.
  2. Within the <ul>, we need to decide if the MenuItem is a text-based menu item or a divider.  We do this by detecting if the divider property exists and/or there is text to display.  If the MenuItem is a divider, the special divider class is applied to a <li> and Bootstrap renders it as a thin gray line.
  3. If the MenuItem is actually a text-based item, we style it appropriately in its <li> element.  Notice the binding to command.canExecute().  In KoLite, if you provide a canExecute() function on a command (with is a computed observable), it can determine if the command can be executed or not.  In this case, we want the UI to gray-out or disable if the command cannot execute.  Once the command can execute, it will immediately synchronize the UI element to be a clickable command.
  4. Inside the <li>, we check to see if the MenuItem is a sub-menu or not.  If it is not, we create an <a> element as desired by Bootstrap to create the link to click on, binding the appropriate command to it.  We also bind the text in a <span> element inside the <a>.
  5. If the MenuItem does have a sub-menu, we assume that the MenuItem can’t be clickable, but instead, simply groups other elements.  In that case, we call upon the Durandal compose binding handler to recursively call this view sending the MenuItem as the view model to bind to in that context.

Now, to render the top-level menu bar, in our main view we’d add the following markup (I’m using nav-pills in Bootstrap to represent the top-level menu items, but you don’t need to do that):

<ul class="nav nav-pills menu-bar" data-bind="foreach: menus">
    <li class="dropdown">
        <a class="dropdown-toggle" href="#" data-toggle="dropdown" role="button" data-bind="text: text"></a>
        <!-- ko compose: { view: 'views/menu.html', model: $data } -->
        <!-- /ko -->
    </li>
</ul>

This assumes that the view model for the main view has a collection of Menu objects in an observableArray called menus.

It renders a <li> element for each Menu giving it a dropdown class.  The <a> element will trigger Bootstrap to open the <ul> element that immediately follows the <a>.  That <ul> is generated by a compose binding calling onto the Menu view to render that one Menu and all of its children recursively.

No Comments