Progressive, In-Place Menu Navigation with jQuery & Knockout

Progressive navigation is a pretty common theme throughout the web - we see hierarchical tree menus on half the sites we visit - but a menu structure doesn’t always fit.

Sometimes it is preferable to be able to guide a user through a series of progressively more specific options one step at a time - almost like a small-scale wizard.

Progressive Menu

This way the user still gets the ability to dig through menus, but they only see one step at a time and we don’t use up a bunch of space on our UI.

I have recently written something along these lines for a project so I thought I’d add it to my utils library. You can see an example of the final version running here.

Defining the Structure

To define the hierarchy of links, I decided to use a data- attribute to denote groups of links:

<a href="#" data-nav-group="group1">Link in group 1</a>
<a href="#" data-nav-group="group1">Another link in group 1</a>

<a href="#" data-nav-group="group2">Link in group 2</a>
<a href="#" data-nav-group="group2">Another link in group 2</a>

To define the navigation between groups I used another data attribute to specify the target for each link. If no target is specified then the link will be ignored (though any other click handler or href attribute would be treated normally):

<a href="#" data-nav-group="group1" data-nav-target="group2">Link to group 2</a>
<a href="#" data-nav-group="group1">Link to Nowhere</a>

This non-hierarchical method means that links can go from any group to any other group as required.

Plugin Implementation

The plugin needs to do 3 things:

  1. Handle click events to select a specific group
  2. Hide elements that aren’t in the selected group
  3. Fade in elements that are in the selected group

Let’s look at these one at a time.

Handling Click Events

Whenever a user clicks on an element with a data-nav-target attribute, we want to select the group specified by that target: nice and simple with the jQuery ‘on’ method:

$.fn.progressiveNav = function() {
  var $this = $(this),
    showGroup = function(groupName) {
      //to be implemented
    };

  $this.off('click.nav').on('click.nav', '[data-nav-target]', function() {
    showGroup($(this).attr('data-nav-target'));
  });
};

Here we firstly use off with a namespaced click event to remove any existing nav handlers, and then use on to attach to any element that has the required attribute. When the click event is invoked, we call a showGroup method (which we’ll implement in a minute) with the value of the attribute from the clicked element.

Showing a Group

To show a group, we want to hide anything that doesn’t match the current group, and fade-in anything that does match. We can achieve this easily enough with a combination of attribute selectors and the hide and fadeIn methods:

var showGroup = function(groupName) {
  $this.find('[data-nav-group!=' + groupName + ']').hide();
  $this.find('[data-nav-group=' + groupName + ']').fadeIn();
};

This will immediately hide anything outside of the specified group, and will fade in the new group without causing the UI to jump about.

Showing the Initial Group

When we first invoke the progressiveNav function, we want to display either a parameter-specified group, or the first group found.

Again, nothing too complicated:

$.fn.progressiveNav = function(startGroup) {
  //...hook up click event as before...

  //grab the first group if none was specified
  if (!startGroup) {
    startGroup = $this
      .find('[data-nav-group]')
      .first()
      .attr('data-nav-group');
  }

  //show the first group - either passed in or the first one found
  showGroup(startGroup);
};

If the user has specified a group name then we just pass that to our showGroup method. If not, we use the same attribute selectors as earlier to grab the first group attribute we find.

You can see a working example of everything so far in this jsFiddle.

Extending for Knockout

If we want to be able to set and retrieve the current group using Knockout then we need to do a little bit more work. Specifically, we’ll need to create a custom binding so that we can specify our navigation container as below:

<!-- HTML -->
<div data-bind="progressiveNav: level">
  <!-- grouped links as before -->
</div>
//JavaScript
var viewModel = {
  level: ko.observable()
};

ko.applyBindings(viewModel);

Setting the Group from Knockout

To allow changes in the view model to be reflected in the navigation, we can use the update method of the custom binding to call the progressiveNav plugin method we created earlier:

ko.bindingHandlers.progressiveNav = {
  update: function(element, valueAccessor) {
    $(element).progressiveNav(ko.utils.unwrapObservable(valueAccessor()));
  }
};

Updating the view model property from click event handlers, on the other hand, is slightly harder…

Updating View Model from Click Handler

The issue here is that, at this point, there is no way for the custom binding to know that someone has clicked on one of the navigation links, so there is no way for it to update the view model.

We can remedy this by adding a custom jQuery event to the showGroup method from earlier that will notify subscribers whenever the selected group is changed:

var showGroup = function(groupName) {
  //show & hide groups as before

  //raise an event on the element to notify that it was updated
  $this.trigger('navchanged', groupName);
};

We can now subscribe to that event in the init function on the custom binding to update the source observable:

ko.bindingHandlers.progressiveNav = {
  init: function (element, valueAccessor) {
    var setter = valueAccessor();

    $(element).on("navchanged", function (e, groupName) {
      setter(groupName);
    });
  }
  update: function (element, valueAccessor) { /* as before */ }
};

Now we can update the view model property from the view, and update the view from the view model. You can see all this working here.

The source code for the combined jQuery & Knockout versions can be found on GitHub.

Enjoy!