Static content is so yesterday—Web sites today are all about staying fresh and current. To that end, on a recent internal project I created a new intranet site and opted to develop a dynamically populated menu. I went this route so that managing the links didn't involve having to open up the HTML files each time new content was added. This made updating the menu much less trouble for me as well as for the content owners.
Before the code
We'll start with designing the database. We can't write code to access a database until we know how the database is designed. To accomplish our menu we only need two tables, no stored procedures or anything else (though for managing the data in the menu, a stored procedure would be the easiest method). So you can essentially use any database system you'd like.
Figure A shows the database tables we'll be using for our menu. The MENU_ITEMS table is where all of the actual menu tree items will live. Both the menu name (menu_item_name) and HREF (menu_item_url) are stored, along with the primary key (menu_item_id). The table MENU_ITEMS_XREF holds the structure of the tree itself. Both menu_item_id and child_id refer to the menu_item_id from the MENU_ITEMS table. When an ID is in the child_id column, it indicates that the ID will be nested under the item in the menu_item_id column. Figure B illustrates this concept.
The last column in the MENU_ITEMS_XREF table is the sort_order field. Initially, I just let the menu query that generates the menu handle the sorting (alphabetically). Wouldn't you know it though; the business unit had other ideas. So now there is a sort_order field. This allows the content owner to place more important or frequently visited topics closer to the top of the page.
That's it for the database: Let's code
The code we use to create the menu is basically a custom ColdFusion tag that recurses over a database structure and builds the appropriate menu structure as it goes. We'll break down the pieces of create_menu.cfm and discuss each section.
The code in Section A defines some initial parameters we will need as default. You may or may not need this, depending on whether your site has pages in subfolders and such. Our site did, so we needed a way to get up the folder tree to the images directory and perform similar tasks.
<CFPARAM DEFAULT="1" NAME="ATTRIBUTES.Level">
<CFPARAM DEFAULT="" NAME="ATTRIBUTES.divID">
<CFIF ATTRIBUTES.Level EQ 1>
<CFSET VARIABLES.RootLevel = "">
<CFELSEIF ATTRIBUTES.Level EQ 2>
<CFSET VARIABLES.RootLevel = "../">
<CFELSEIF ATTRIBUTES.Level EQ 3>
<CFSET VARIABLES.RootLevel = "../../">
var menu, arrowImg;
menu = document.getElementById("d" + n);
// Determine if the menu is currently showing.
if (menu.style.display == 'block')
// If it is showing, hide the menu and update the twisty image.
menu.style.display = 'none';
arrowImg = document.images['i' + n];
arrowImg.src = "#VARIABLES.RootLevel#images/right_arrow.gif";
Section C handles the event of clicking on an already expanded menu branch. Doing this triggers the branch to collapse. This doesn't have to happen, but why make someone select a different level just to collapse a section? We simply take the section the user clicked on and see if it's expanded; if it is, collapse it. If it is not expanded, move right along to Section D.
// Hide all layers first.
vardivs = document.getElementsByTagName("div");
for (vari = 0; i < divs.length; i++)
if (divs[i].id.indexOf("d0") >= 0)
divs[i].style.display = 'none';
// Reset the images.
for (var j = 0; j < document.images.length; j++)
if (document.images[j].src.indexOf("down_arrow") > 0)
document.images[j].src = "#VARIABLES.RootLevel#images/right_arrow.gif";
// Show the menus and update their twisty images.
i = 2;
while (n.length >= i)
menu = document.getElementById("d" + n.substring(0, i));
arrowImg = document.images["i" + n.substring(0, i)];
menu.style.display = "block";
arrowImg.src = "#VARIABLES.RootLevel#images/down_arrow.gif";
i += 2;
This menu is the product of a ColdFusion custom tag. Wherever you need the menu (keep in mind this menu is vertical), you just include the tag. To set up the menu on the page it will be used in requires two lines of code:
<body <CFIF IsDefined('URL.Div')>
The code above shows the <body> tag as well as the actual custom tag call. The ColdFusion code in the body tag is used so that the menu will expand to where you were when you follow a link. Without this logic, the menu collapses on load by default.
We don't call the custom tag in the standard <CF_> syntax. We call it using <CFINCLUDE> because it is designed to call itself as it builds the tree, so we just need to include the code.
Back to the custom tag now, we'll continue with the ColdFusion code. Section E establishes our variables and queries the database. You'll notice the query is actually a query of a variable. You can execute this section in whichever way you are most comfortable. I chose to store my initial query as an APPLICATION scoped variable since the menu data doesn't change very frequently and because the menu is located on each page, I wanted to spare my database the extra traffic. The last query in Section E is the query that creates the application variable.
<CFPARAM DEFAULT="0" NAME="ATTRIBUTES.MenuID">
<CFPARAM DEFAULT="" NAME="ATTRIBUTES.DivID">
<CFPARAM DEFAULT="0" NAME="VARIABLES.LoopCount">
<CFPARAM DEFAULT="0" NAME="VARIABLES.IncreNum">
<CFLOCK SCOPE="APPLICATION" TIMEOUT="10" TYPE="READONLY">
<CFSET VARIABLES.qGrabMenu = APPLICATION.qGrabMenu>
<CFSET VARIABLES.DataSource = APPLICATION.DataSource>
<CFQUERY DBTYPE="query" NAME="qGetMenuElement">
WHERE Menu_item_ID = #ATTRIBUTES.MenuID#
ORDER BY sort_order
<CFQUERY DATASOURCE="#APPLICATION.DataSource#" NAME="APPLICATION.qGrabMenu" CACHEDWITHIN="#APPLICATION.QueryTimeOut#">
(SELECT menu_item_name FROM MENU_ITEMS WHERE MENU_ITEMS.menu_item_id = menu_items_xref.menu_item_id) AS Parent,
(SELECT menu_item_url FROM MENU_ITEMS WHERE MENU_ITEMS.menu_item_id = menu_items_xref.menu_item_id) AS Parentlink,
(SELECT menu_item_name FROM MENU_ITEMS WHERE MENU_ITEMS.menu_item_id = menu_items_xref.child_id) AS Child,
(SELECT menu_item_url FROM MENU_ITEMS WHERE MENU_ITEMS.menu_item_id = menu_items_xref.child_id) AS Childlink,
Section F rounds out our code; it's the final piece of our create_menu.cfm custom tag. Using recursion we're able to build and populate the menu tree with a single custom tag. Create_menu.cfm uses recursion to call itself as a custom tag in order to build subbranches off the main root menu as needed. When a subbranch is needed, the code calls <CF_create_menu>, which processes the data for the new branch. Should additional subbranches be needed from an existing subbranch, the code is called again and recurses another level (or as many levels as needed) to create the branch or branches. As each level completes, the one above continues on. After all of the recursions have run, we're left with the menu tree.
<!—- Build menu —->
<!—- Increment the loop counter —->
<CFSET VARIABLES.LoopCount = VARIABLES.LoopCount + 1>
<CFIF (qGetMenuElement.childlink NEQ '')>
<!—- Last element on a branch —->
<!—- build out branches —->
<CFSET VARIABLES.IncreNum = "0" & VARIABLES.LoopCount>
<CFSET VARIABLES.Num = ATTRIBUTES.DivID & VARIABLES.IncreNum>
<a href="##" class="menu" onclick="ShowMenu('#VARIABLES.Num#'); this.blur(); return false"><imgsrc="#VARIABLES.RootLevel#images/right_arrow.gif" name="i#VARIABLES.Num#" width="9" height="9" border="0" hspace="4 /">#child#<!—- -#ATTRIBUTES.divID# —-></a>
<div id="d#VARIABLES.Num#" class="expand">
<CF_create_menumenuID = "#child_id#" divID = "#VARIABLES.Num#" level = "#ATTRIBUTES.Level#">
<!—- Reset the loop counter —->
<CFSET VARIABLES.LoopCount = 0>
That's all there is to it. We've created a dynamic collapsible tree menu using a database and ColdFusion. You can download the files necessary to create your own dynamic menu here. And a working sample is available for viewing here.