Data Management

A recursive, dynamic menu tree in ColdFusion and JavaScript

It's easy to develop a database-powered dynamic menu, one that doesn't take up a lot of page real estate, yet lets the user explore to find the section they want. We'll show you how, with a little bit of ColdFusion and JavaScript magic.


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.

So why go through the trouble of building a dynamic menu in ColdFusion when an HTML/JavaScript menu could be written and maintained with relative ease? Sure, we could have built the menu in HTML and JavaScript and just modified the code as needed, but where's the fun in that? More importantly, project maintenance wasn't what I was looking for. I didn't want to be tied to the project in perpetuity; the team I was on was too small and had too many new projects, leaving no time for maintenance of old projects. The end users and I came up with a menu that could be managed easily, and (here's where the tree parts comes in) took up very little initial space on the page. The reason why space was an issue was that the site had several embedded sections, which had several embedded sections, which had… you see where I'm going.

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.

Section A
 
<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 = "../../">
</CFIF>

The following sections of code all combine into a single JavaScript block, but to make it easier to explain, I have broken it up into smaller, more digestible pieces, beginning with Section B. This is the JavaScript that will handle the menu, collapsing and expanding the appropriate branches and changing the image states of those branches.

Section B
 
<CFOUTPUT>
 <script type="text/javascript">
   functionShowMenu(n)
    {
     var menu, arrowImg;
     menu = document.getElementById("d" + n);

Section C
 
      // 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.

Section D
 
     else
      {
        // 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;
        }
      }
    }
  </script>
</CFOUTPUT>

Section D is pretty long—it's the remainder of the JavaScript function ShowMenu(). If the branch that the user selected was not already expanded, the first order of business is to collapse all branches. We essentially want to start off with a fresh menu each time. To accomplish this, we loop over the structure of the image tags (all named in series), resetting each image. We repeat the process on the menu structure itself (each <div> tag that makes up the menu is named in a series like the images, so looping over the entire menu is very easy).

Once the entire menu has been collapsed, we have to expand the chosen menu branch. Since branches can have their own branches, the JavaScript has to not only expand the main branch but, if the user clicked a subbranch, that branch has to be expanded also (Figure C). Once the appropriate branches are expanded, the corresponding images need to be changed. Our menu uses two images: a blue arrow pointing to the right and a gold arrow that points down. These serve as an additional visual cue for the user.

Once the images are adjusted, we're done. Well, actually, we're not, but the JavaScript is. We covered the JavaScript first, even though the ColdFusion part of our custom tag will execute first. I wanted to establish the flow of the code and how it should work before we cover the setup. The JavaScript is the workhorse of this menu, so it's important to know what's going on there and how everything works.

The setup
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')>
onload="ShowMenu('<CFOUTPUT>#URL.Div#</CFOUTPUT>')"</CFIF>>
 
<CFINCLUDE TEMPLATE="modules/menu/create_menu.cfm">

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.

Section E
 
<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>
</CFLOCK>
 
<CFQUERY DBTYPE="query" NAME="qGetMenuElement">
    SELECT *
    FROM VARIABLES.qGrabMenu
    WHERE Menu_item_ID = #ATTRIBUTES.MenuID#
    ORDER BY sort_order
</CFQUERY>
 
<CFQUERY DATASOURCE="#APPLICATION.DataSource#" NAME="APPLICATION.qGrabMenu" CACHEDWITHIN="#APPLICATION.QueryTimeOut#">
SELECT
      menu_item_id,
        (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,
      child_id,
        (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,
   sort_order
    FROM menu_items_xref
</CFQUERY>

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.

Section F
 
<!—- Build menu —->
<CFLOOP QUERY="qGetMenuElement">
   <!—- Increment the loop counter —->
    <CFSET VARIABLES.LoopCount = VARIABLES.LoopCount + 1>
    <CFIF (qGetMenuElement.childlink NEQ '')>
       <!—- Last element on a branch —->
        <CFOUTPUT>
        <a href="#VARIABLES.RootLevel##childlink#" class="menu"><imgsrc="#VARIABLES.RootLevel#images/transparent.gif" width="9" height="9" border="0" hspace="4">#child#<!—- -#ATTRIBUTES.divID# —-></a><CFIF ATTRIBUTES.divID NEQ ""><CFIF FindNoCase(qGetMenuElement.ChildLink, CGI.PATH_INFO)><script type="text/javascript">ShowMenu('#ATTRIBUTES.divID#');</script></CFIF></CFIF>
        </CFOUTPUT>
       <!—- build out branches —->
    <CFELSE>
        <CFOUTPUT>
        <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#">
            </div>
        </CFOUTPUT>
    </CFIF> 
</CFLOOP>
<!—- 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.

Editor's Picks