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.
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:
- Handle click events to select a specific group
- Hide elements that aren’t in the selected group
- 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!