Dynamic Menu and Content Loader Utility with Knockout
This article describes a utility library that builds a menu from an XML data source with Knockout JS.
Additionally, the library also loads content based on the URL # tags. This was originally built to support semi-static content for various projects. The simplest way to see this is action is to take a clone of the library and its demo from Git Hub
https://github.com/darren-h-gill/KnockoutXMLNavMenu
I have also uploaded the code here
Download demo
If I make any changes or improvements to the library I will do so in the GitHub repository, so the download above may become stale over time!
The menu itself is simply styled <a> tags and buttons, the styling is from the W3 Schools w3.css library. I won’t delve too deeply into the stylistic elements used because the W3 Schools web site already does a fine job of that!
The menu itself is displayed using KnockoutJS, with the structure loaded dynamically by jQuery
The code from the demonstrator project has a single entry point, index.html
It looks like this:-
I will discuss the library’s as two sections, the menu building part and the dynamic content loading part. To follow best practice I really should refactor this utility into two libraries, since the two parts really have very little overlap. However, pragmatically I tend to use both sets of function at the same time so it is convenient to me to keep them together!
Building the Menu
Every menu needs to be defined somewhere. In this example it’s a static XML file, but could just as easily have been from some API resource that produces such output from the database. This is what I do in practice to generate my static menu files. Discussing that is beyond the scope of what this article is about, but it’s easily found on the internet. If you want to generate this sort of XML from a SQL Server, then you could start by looking at the answer to this stack exchange question
https://stackoverflow.com/questions/14765937/cte-and-for-xml-to-generate-nested-xml
Menu Definition
The menu itself is quite a simple XML structure.
The structure could be even simpler, but I have allowed for multiple menu trees to be held in the same resource. I will discuss how the correct menu structure is picked later when looking at the JavaScript that uses this.
You can view this sample XML source directly on GitHub, see
https://github.com/darren-h-gill/KnockoutXMLNavMenu/blob/master/wwwroot/data/nav.xml
The document element is menus. The remaining structure is a set of menu elements(at least one!) each containing a number of menuitem elements. Although there is no DTD or schema there are a couple of obvious rules.
Each menu should have at least one menuitem element. A menuitem must have a name element, this will be used as the onscreen caption. A menuitem will usually have a url element. Menuitem elements can also have a nested set of sub menuitem elements. There is no limit on the number of levels you could go, but common sense should prevail!
There are two other elements that can be specified, target is used to identify where a browser would open a navigated resource. If omitted, this is assumed to be “_self”. Finally, you can add a popup element. When specified generated href attribute will be converted to a JavaScript call where the content identified by the url element will be dynamically loaded and shown in a lightbox type display. This is partly why I have not factored out the content loading from the menu building! More on this later.
Structure of the HTML
Using this library has a few requirements of your HTML, but not much so I am going to show you most of it in one go!
<!DOCTYPE html>
... ... ... html/head Content ... ... ...
<link rel="stylesheet" href="//www.w3schools.com/w3css/4/w3.css">
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Lato">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<script src="//ajax.aspnetcdn.com/ajax/jQuery/jquery-3.2.1.min.js"></script>
<script src="//ajax.aspnetcdn.com/ajax/knockout/knockout-3.4.2.js"></script>
<script src="scripts/navControl.js"></script>
<script id="subMenuTemplate" type="text/html">
<!--
<div class="w3-dropdown-hover">
<button
class="w3-button w3-bar-item w3-padding-large"
data-bind="text:name">Dropdown Name</button>
<div class="w3-dropdown-content w3-bar-block w3-card-4"
style="margin-left:2em"
data-bind="template: {name: 'subMenuTemplate', foreach: items}">
</div>
</div>
<!--
<!--
<a
class="w3-bar-item w3-button w3-padding-large w3-hide-small"
data-bind="attr: {href: url || '#', title: name, target: target}">
<span data-bind="text: name">Menu Name</span>
</a>
<!--
</script>
<script type="text/javascript">
jQuery(window).ready(function () {
navControl.buildMenu("data/nav.xml", 1, function (oMenuRoot) {
ko.applyBindings(oMenuRoot, document.getElementById("topNav"));
}, null);
});
</script>
</head>
<body>
<div id="topNav">
<div
class="w3-bar w3-black w3-card-2"
data-bind="template:{name: 'subMenuTemplate', foreach: items }">
</div>
</div>
<div id="pageMain">
... ... ... Body Content ... ... ...
</div>
</body>
</html>
At first glance, you may notice that there is very little JavaScript running here! It’s mostly hidden in the navControl.js
script. Before jumping into that, take a look at the stylesheets and script library references that are found in the head
section.
As I mentioned at the beginning of the article, there are three key libraries that this is built upon.
- The W3 Schools open source CSS framework
<link rel="stylesheet" href="//www.w3schools.com/w3css/4/w3.css">
- jQuery (here using version 3.2.1)
<script src="//ajax.aspnetcdn.com/ajax/jQuery/jquery-3.2.1.min.js"></script>
<script src="//ajax.aspnetcdn.com/ajax/knockout/knockout-3.4.2.js"></script>
And finally, the navControl.js
library that is the centre of this article.
<script src="scripts/navControl.js"></script>
There are two other script tags in the head, but one of these does not have any JavaScript. Instead it has a knockout template HTML definition.
<script id="subMenuTemplate" type="text/html">
<!--
<div class="w3-dropdown-hover">
<button
class="w3-button w3-bar-item w3-padding-large"
data-bind="text:name">Dropdown Name</button>
<div class="w3-dropdown-content w3-bar-block w3-card-4"
style="margin-left:2em"
data-bind="template: {name: 'subMenuTemplate', foreach: items}">
</div>
</div>
<!--
<!--
<a
class="w3-bar-item w3-button w3-padding-large w3-hide-small"
data-bind="attr: {href: url || '#', title: name, target: target}">
<span data-bind="text: name">Menu Name</span>
</a>
<!--
</script>
Notice that this script tag has an id attribute of “subMenuTemplate
”, this will be used by the Knockout library to generate content when it is bound to a view-model. Notice in the middle of this HTML is some Knockout binding syntax that references a template named subMenuTemplate
. The template is recursive!
So, you can see how the nesting of menu item tags in the data structure might be converted into HTML markup in the browser. This template can be summarized by the following pseudo code
Menu Item Processing Pseudocode
For a given menu item that has some nested menu items then render a button followed by a drop down container. Inside the drop down container process each of the nested menu items.
If the menu item did not have and nested items, then render an anchor anchor tag using the name, url and target properties of the menu item
Starting the Menu
As with all recursive functions and structures, there must be some original starting point. In this case it’s in the first piece of markup of the HTML body tag.
<div id="topNav">
<div
class="w3-bar w3-black w3-card-2"
data-bind="template:{name: 'subMenuTemplate', foreach: items }">
</div>
</div>
The outer div here his given an id of topNav. This is referenced in the remaining JavaScript.
jQuery(window).ready(function () {
navControl.buildMenu("data/nav.xml", 1, function (oMenuRoot) {
ko.applyBindings(oMenuRoot, document.getElementById("topNav"));
}, null);
});
This is standard jQuery ready functionality. The anonymous function runs as soon as the document is all loaded and available for manipulation. I simply call the buildMenu
method of the navControl
library. The method has the following signature: -
navControl.buildMenu = function (url, menuID, cbReady, cbError)
The url
parameter identifies the source of the XML menu structure.
The menuID
parameter is used to identify which particular menu in the structure to build. It can either be a name string or a 1 based number index into the structure.
The cbReady
parameter is crucial. It’s a callback function that will pass back an oMenuRoot
object when the method has completed. The process is asynchronous so this call will finish immediately before the menu is ready! You need to be aware of this. A common practice would be to have the placeholder hidden until you are ready to display the completed content.
The cbError
parameter is an optional callback that will pass an error string. I have made the call passing null to illustrate that there is a 4th parameter available!
Building the Menu
Now that I have introduced the HTML, it’s time to look at the JavaScript library itself. From the discussion above you will have noticed the call to build the menu was implemented like this: -
navControl.method();
The library has been written following the revealing module pattern. In practice, this means that it is written like this: -
(function (navControl, $, undefined) {
var _my_private_varable = false;
navControl.myMethod = function(x,y,z){};
}(window.navControl = window.navControl || {}, jQuery));
To understand the mechanism of hiding the internal workings of the library you have to start from the end! The whole outer function is executed as soon as it loads passing either an existing reference to the library or an empty object. I also pass a convenience reference to the jQuery object. This is because I plan to use the $ name inside the library and I don’t want the code falling foul of any other libraries that might happen to hijack this! Inside the module I know that $ is guaranteed to be the jQuery selector function!
The buildMenu Method
Having skimmed over the module pattern, lets look at the exposed buildMenu
method in detail. I am going to edit out some of the logging and error checking from the listing below for brevity.
navControl.buildMenu = function (url, menuID, cbReady, cbError) {
var fMenuWalker = function (oContainer, tagContainer) {
var aMenuItem = [];
$(tagContainer).children().each(function (idx, tag) {
if (tag.tagName === "menuitem") {
var newMenuItem = {
name: $("> name", tag).text(),
url: $("> url", tag).text(),
target: $("> target", tag).text() || "_self",
popup: $("> popup", tag).length ? true : false
};
if (newMenuItem.popup) {
newMenuItem.url = "JAVASCRIPT:navControl.dynamicPopup('"
+ newMenuItem.url
+ "','" + newMenuItem.name + "')";
}
fMenuWalker(newMenuItem, tag);
aMenuItem.push(newMenuItem);
}
});
if (aMenuItem.length) {
oContainer.items = aMenuItem;
}
return;
};
$.ajax({
url: url,
dataType: "xml",
success: function (data, statusText, jqXHR) {
var sSelector = "menus > menu";
if (menuID) {
if (typeof menuID === "number") {
sSelector += ":nth-child(" + menuID + ")";
} else {
sSelector += "[id='" + menuID + "']";
}
}
try {
var tagMenu = $(sSelector, data).first().get();
if (!tagMenu) throw "Could not find menu!";
var retMenu = {};
fMenuWalker(retMenu, tagMenu);
console.log("buildMenu completed sucessfully. Invoking callback ...");
cbReady(retMenu);
} catch (ex) {
var sErrorMessage = ex.message || "No Message!";
console.error("navControl.buildMenu Error processing menu DOM\n"
+ sErrorMessage);
if (cbError && typeof cbError === "function") cbError(sErrorMessage);
return;
}
},
error: function (jqXHR, statusText, errorText) {
}
});
};
As you can see, this method has two parts: a recursive function to process an XML DOM element that builds up a JavaScript literal object and an asynchronous call made using jQuery to fetch the XML DOM.
Assuming that the method was called with a valid Url parameter and nothing untoward occurred in the AJAX request the success handler will eventually fire. The first step at that point is to make a query of the DOM. I use jQuery to do this because it has a very natural syntax (if you are used to CSS!) that should be browser independent.
var sSelector = "menus > menu";
if (menuID) {
if (typeof menuID === "number") {
sSelector += ":nth-child(" + menuID + ")";
} else {
sSelector += "[id='" + menuID + "']";
}
}
In our call from the index.html file we passed in the number 1. So, the selector that would come out of this would be: -
menus > menu:nth-child(1)
What this should be saying to jQuery is “get me the first menu element under the menus document root!
This selector is used in the jQuery call to get the right entry point to our XML structure
var tagMenu = $(sSelector, data).first().get();
Strictly speaking, I should not need the .first() call in the chain, but I am protecting myself from someone using the method like this:-
navControl.buildMenu (goodUrlWithBadStructure, "repeatedID", func(oMU){});
Where the XML returned is like this: -
<menus>
<menu id="repeatedID" >
…
</menu>
<menu id="repeatedID" >
…
</menu>
</menus>
When you use jQuery to navigate an XML document you have to remember that what is returned from a jQuery call is not an element, but a jQuery wrapper structure. So, the final part of the chain use the .get() method to unwrap the underlying element.
The result of this should be that my tagMenu variable is indeed the start of my menu structure! All that remains to be done at that point is to start the call to the recursive menu navigation!
var retMenu = {};
fMenuWalker(retMenu, tagMenu);
cbReady(retMenu);
Notice that I start the recusion by passing an empty object and the <menu> element. Let’s take a brief look at the recursion function fMenuWalker
. This function is defined within the buildMenu method so is not even exposed to other parts of the library!
The function starts by creating an empty array, aMenuItem
. This is probably not the best of naming choices since what it will hold is in fact child menu items!
Again, I use jQuery to work its way down the structure with this call
$(tagContainer).children().each(function (idx, tag) {
if (tag.tagName === "menuitem") {
var newMenuItem = {
name: $("> name", tag).text(),
url: $("> url", tag).text(),
target: $("> target", tag).text() || "_self",
popup: $("> popup", tag).length ? true : false
};
if (newMenuItem.popup) {
newMenuItem.url = "JAVASCRIPT:navControl.dynamicPopup('"
+ newMenuItem.url + "','"
+ newMenuItem.name + "')";
}
fMenuWalker(newMenuItem, tag);
aMenuItem.push(newMenuItem);
}
});
In essence, this works over all the menu Item tags below the current container and creates a new simple JS object with a copy of the key details. Once again I use jQuery to navigate the XML. It is not the most efficient mechanism, but it does keep the code understandable which I consider highly valuable. Notice the selector syntax I use:
$("> name", tag)
The greater than symbol means “child of”, this syntax is usually seen in the this form “ul > li” where you want to get just the first level list items in a list and not anything deeper. Here, I am using it without a left hand argument. This is implied from the context node held in the tag variable .
I do this because there is a big gotcha to be aware of using jQuery when you can find nested structures. Consider this fragment
….
<menuitem>
<name>Resources</name>
<menuitem>
<name>Knockout</name>
<url>http://knockoutjs.com/</url>
<target>_blank</target>
</menuitem>
<menuitem >
<name>W3 CSS</name>
<url>https://www.w3schools.com/w3css</url>
<target>_blank</target>
</menuitem>
</menuitem>
...
When the name of the outer element here was being determined, the code could have been written like this.
name: $("name", tag).text()
That would have produce a menu caption of “ResourcesKnockoutW3 CSS” which is not what I want.
The jQuery call for $(“name”)
would have made an array jQuery wrappers around every name tag below that point, and the chained .text()
call would simple have concatenated all of them! So, the correct call to use is: -
name: $("> name", tag)().text()
After the new JS object representing the menuitem is created, it is pushed onto the array declared previously.
At the end of the function the array of child items is attached to the passed container reference as a new items
property, but only if there are items to add!
if (aMenuItem.length) {
oContainer.items = aMenuItem;
}
The only other code of particular note is to look at the popup element. The popup property of the object is a Boolean that was determined by the existence of a <popup> element. When it is true, the url is modified thus: -
if (newMenuItem.popup) {
newMenuItem.url = "JAVASCRIPT:navControl.dynamicPopup('"
+ newMenuItem.url + "','"
+ newMenuItem.name + "')";
}
I will describe the dynamicPopup method of the library later. All that needs to be said at this point is that instead of directing the browser to another page when the menu item is clicked it will instead demand load the content at the end of the url and display it in a lightbox style panel in the current page.
Finally, once the recursion completes in the AJAX success handler, the JavaScript object will be passed as a parameter to the call back function.
As a reminder, this is the script used to invoke the method back in the index.html file: -
jQuery(window).ready(function () {
navControl.buildMenu("data/nav.xml", 1, function (oMenuRoot) {
ko.applyBindings(oMenuRoot, document.getElementById("topNav"));
}, null);
});
The single line of code in the callback invokes the Knockout Template binding.
ko.applyBindings(oMenuRoot, document.getElementById("topNav"));
Important! The call to apply bindings supplies two parameters. The oMenuRoot
“view model” and an element to scan for Knockout binding syntax. If the element was omitted then the entire document would be scanned. This may work for you, but only if there are no other parts of the page where you want knockout content displayed! I would always recommend passing a reference to a particular scope of document that you need processed. You never know how your pages may evolve other time and it’s best to do it right from the start!
Dynamic Content Loading with Hash Tags
The other part to this library is about handling content to be loaded via AJAX calls. There is no rocket science involved in getting jQuery to load content (just see the dosc at http://api.jquery.com/load ) but what this library does that is not so obvious is work with the hashtag portion of a URL, sometimes known as the bookmark.
When someone follows a link like this:-
http://someserver/path/file.html?searchterm=asd#endofresults
The browser will send
http://someserver/path/file.html?searchterm=asd
to the originating server, and the browser will look inside of the resulting document for an anchor (<a>
) tag with a name attribute matching the text “endofresults” in order to scroll the content with the anchor into view.
If the resulting page contains links to just a # tag, or indeed a fully qualified URL to the same page with a different hash tag then no request is sent to the server. What I needed to be able to do was to intercept these changes to the hash tag. Browsers offer you a hook to react to these changes, and jQuery has a convenient wrapper on that.
As the navControl
library loads it creates a private function to process hash tag navigation called processHash
. It then runs this snippet of code.
$(window).bind('hashchange', function (e) {
processHash(window.location.hash.substring(1));
});
Very simply, the jQuery event binding waits on hash tag change events and triggers a call to the processHash
function with the text following the hash symbol. There is another piece of initialization code that runs: -
var _bookmarkName = null;
if (window.location.hash) {
_bookmarkName = window.location.hash.substring(1);
$(document).ready(function () {
processHash(_bookmarkName);
});
}
Because this library demand loads content then I need it to immediately do that processing when the page is loaded directly with a hash tag specified.
Hash Tag Processing
In essence the processHash function searches the document for a names anchor tag matching the passed parameter. What is does then comes down to three cases.
- If the anchor exists AND it contains content, then the function does nothing other than log the call. It lets the browser do its thing as normal
- If the anchor exists, but is empty, then the function attempts to load new content based on he tag name
- If the anchor was not found at all, then the function attempts to load new content and append it to the foot of the document.
The summarised code of the processHash
function is shown below:-
var processHash = function (sBookmark) {
var sBookmarkSelector = "a[name='" + sBookmark + "']";
var urlContent = "content/" + sBookmark + ".html";
var tagBookmark = $(sBookmarkSelector).get(0);
if (tagBookmark) {
if (tagBookmark.innerHTML) {
} else {
$.ajax(urlContent, {
success: function (data, textStatus, jqXHR) {
tagBookmark.innerHTML = data;
},
error: function (jqXHR, textStatus, errorThrown) {
}
});
}
} else {
$.ajax(urlContent, {
success: function (data, textStatus, jqXHR) {
var jqBookmark = $("<a name='" + sBookmark + "'></a>");
tagBookmark = jqBookmark.get(0);
tagBookmark.innerHTML = data;
$("body").append(tagBookmark);
$('html, body').animate({
scrollTop: jqBookmark.offset().top
}, 1000);
},
error: function (jqXHR, textStatus, errorThrown) {
});
}
};
You can see all this is action if you download and run the demonstrator website. If you scroll to the bottom of the document you will see: -
As the text explains there is an empty bookmark between the first and second paragraphs of “Section2”
The more link targets that bookmark , its address is
http://localhost:62737/wwwroot/index.html#section2
When I click it the display changes to this
What has happened here is that an AJAX request has been sent to get content from “content/section2.html” and the content injected following a successful GET operation.
This has demonstrated case (2) of the processing plan specified above. The third case is essentially the same, the only difference being that a new bookmark has to be created first. This is again done using jQuery: -
.........
var jqBookmark = $("<a name='" + sBookmark + "'></a>");
tagBookmark = jqBookmark.get(0);
tagBookmark.innerHTML = data;
$("body").append(tagBookmark);
.........
On subsequent hash changes to the dynamically loaded content, the bookmark would be found to possess content, so it would be treated like case 1 and no duplicate downloads would occur.
Processing content like this is a fairly niche requirement, but comes in handy when you don’t want to clutter your page with rarely accessed and lengthy content, for example terms and conditions, privacy policies etc.
Dynamic Popups
There is one final piece of utility in the navControl library that may be of interest and that is the dynamicPopup
method. This was mentioned earlier in the menu generating code where a menu item could be marked as “Popup”.
The basic mechanism is to create a new DIV tag with the CSS classes: w3-container w3-modal w3-animate-zoom
. Inside that would be some more placeholder content into which a dynamic load of content from the Url is made. This is the result.
The close icons here are taken from the Font Awesome styling that was include at the start. Additionally, to improve the UX the Escape keyup event of the document is hooked when such a panel is visible to cause it to close.
The code is as follows: -
navControl.dynamicPopup = function (url, options) {
var effectiveOptions = {};
$.extend(effectiveOptions, _defPopupOptions);
if (typeof options === "object") {
$.extend(effectiveOptions, options);
} else if (typeof options === "string") {
effectiveOptions.popupPanelName = options;
}
if (!effectiveOptions.popupPanelID) effectiveOptions.popupPanelID = "dynamicPanel" + _dynamicDivPanelCounter++;
var sTemplate = _templatePopup;
sTemplate = sTemplate.replace(/\[\[popupPanelID\]\]/g, effectiveOptions.popupPanelID);
sTemplate = sTemplate.replace(/\[\[popupPanelHeaderCSS\]\]/g, effectiveOptions.popupPanelHeaderCSS);
sTemplate = sTemplate.replace(/\[\[popupPanelName\]\]/g, effectiveOptions.popupPanelName);
var $content = $(sTemplate);
$("body").append($content);
$.ajax(url, {
success: function (data, textStatus, jqXHR) {
$("#" + effectiveOptions.popupPanelID + "_content").get(0).innerHTML = data;
navControl.modal(effectiveOptions.popupPanelID);
}
});
};
What I have not shown here is the markup used as the template for the new content. However, you can see from the screen shot what sort of content would be required. Also, you can see from the use of the sTemplate.replace
calls where the tokens in [[xxxxxx]] are replaced to produce relevant content.
Hopefully, you may find some use from this library. I have used it as a starting point on a few projects, especially where
History
Version 1 (of the article) 21st June 2017