Summary
A very common request we get asked about in the lab is how to generate nested list navigation using WCM; it's not an easy question to answer as the navigator component does not generate nested markup.
The typical solutions that have been implemented in the past involve either post-processing the result of a navigator by parsing and re-writing the markup, or using the WCM API to generate your custom navigation, which necessitates implementing your own strategies for avoiding repeated database lookups.
This article will outline a much simpler method that uses two JSP scripts to generate the UL and LI tags inside the navigator component. This means no post-processing, and the built in caching of the WCM navigator is utilised.
The markup that is generated will use a simple CSS pattern that allows flexible re-styling.
Required knowledge
Please note that this article expects knowledge of HTML, CSS, JSP, and WCM APIs; and it also expected that you have experience with deploying JSP code and JSP components in WCM.
Also please note that this is sample code only, and is not endorsed or supported by IBM - you must take into account your own requirements, and perform your own functional and performance testing.
That said, it is of course appreciated that if you find any defects or identify missing cases, that you comment on it here so they can be fixed; interesting enhancements would be nice to hear about too.
Sample nested list
I'll start first with some sample data and a sample style, and then show how this is generated in WCM.
The list that will be generated will look something like this sample list here, a few levels of nesting are shown just to give an idea of what the HTML will look like.
A few things to note:
- the contents of the list item tag is rendered using whatever WCM components you desire - for this article I assume just a linked title to a WCM site area or content item
- first item is marked with the class "first" - allows the first item to be styled differently, e.g. to remove an extraneous border
- all the items on the current path are marked with the class "selected" - allows the current path to be styled differently
- items that represent content are marked with "content" - this can be used to make content items appear differently to site areas
- the list is put into a container to style it up - in this case I've put it into a container with a class called "myNav"
- you may wonder why I've used "list" and "item" class selectors instead of just selecting by element type (ie. ul/li) - this is for flexibility, as the same styles can then be used on nested DIVs, SPANs, etc.
<div class="myNav">
<ul class="list">
<li class="item first"><a href="#">Item 1</a></li>
<li class="item selected"><a href="#">Item 2</a>
<ul class="list">
<li class="item"><a href="#">Item 2.1</a></li>
<li class="item selected"><a href="#">Item 2.2</a>
<ul class="list">
<li class="item selected content"><a href="#">Item 2.2.1</a></li>
<li class="item content"><a href="#">Item 2.2.2</a></li>
</ul>
</li>
</ul>
</li>
<li class="item"><a href="#">Item 3</a></li>
</ul>
</div>
To style this list up to look like navigation we define a series of class selectors to take advantage of the pattern of classes defined on the list elements.
The example below is white with borders around items, on rollover the items switch to white on dark background, and the current path is highlighted via a grey background
The indentation is the most difficult part in here - ideally you could use the nesting of the lists to do the indentation BUT this makes it impossible to have it act like a navigation bar where each item's border and background fill the whole navigation column.
Instead the indentation uses a series of nested class selectors to add padding - the definition below will indent up to the 3rd sub level, and then further levels would not indent any further; further nested class selectors could be added of course.
You're other option instead of using nested class selectors is to use the
tag in WCM to add indentation via padding with or something other characters.
/* base styles for background and text */
.myNav {background-color: #ffffff; width:200px; font-family: Verdana, Arial, Helvetica; font-size:0.8em; font-weight:bold;}
.myNav a{color: #000000; display:block; padding: 15px 6px 6px 15px; text-decoration:none}
/* put border around top level list only*/
.myNav .list{list-style-type:none; padding: 0px; border: 2px groove}
.myNav .list .list{border:none;}
/* add border on items - note border removed on first item to avoid duplicate border */
.myNav .item{border-top: 2px groove}
.myNav .first{border-top-style: none;}
/* highlight current items */
.myNav .selected > a {background-color: #DDDDDD;}
.myNav .content > a{text-decoration: underline}
/* indenting for each level */
.myNav .item .item > a {padding-left: 35px;}
.myNav .item .item .item > a {padding-left: 55px;}
.myNav .item .item .item .item a {padding-left: 55px;}
/* hover - has to come last to override other styles */
.myNav a:hover{background-color:#999999; color: #ffffff}
The above list looks like this with the styles applied:

Nested list generation
Ok, so now we have a sample list, and a sample style for it, how do we get this generated in WCM?
The solution is to use two pieces of JSP code to generate the nested list tags:
- The first JSP is executed for each item in the navigator, prior to rendering the contents of each item - this JSP's function is to check for level changes and render <ul> and <li> tags appropriately
- The second "cleanup" JSP is executed in the footer - it closes off any hanging lists left over after the items have all been rendered
To keep track of state during rendering, request attributes are used - these are scoped via a string passed into the JSP to avoid naming conflicts.
Here is the first script - lets call this RenderNestedListResult.jsp - and it is to be executed prior to each item:
- there are three state variables being stored - the current level, a flag to indicate the first execution, and the base level - which was the level of the first item
- the level attribute is used to detect a level change since the previous execution - up a level, down one or more levels, or staying at the same level
- note that the nav can only ever go down one level at a time, but may come up multiple levels - so coming up a level goes into a loop to close off the appropriate number of lists
- the base level attribute is used in the "cleanup" script to figure out how many hanging lists there are when the navigator has finished rendering
- the level numbers are generated by getting the current item's path and counting the number of slashes in it (using the split() function)
- this is not the level number within the site, but the level numbers are only used to detect a relative change in level and so their absolute value does not matter
- the selected path is highlighted with the "selected" class - this is done by using a startsWith() test of the current result path against the current context path
- this script does NOT render any WCM content - you do that as usual in the navigator using WCM rendering tags
<%@ page import="java.util.*,java.io.*,java.lang.*,com.ibm.workplace.wcm.api.*" %>
<%@taglib uri="/WEB-INF/tld/wcm.tld" prefix="wcm" %>
<wcm:initworkspace user="<%= request.getUserPrincipal() %>">login fail</wcm:initworkspace>
<%
String SCOPE = request.getParameter("scope");
String LEVEL_ATTR = SCOPE + "_Level";
String ISFIRST_ATTR = SCOPE + "_IsFirst";
String BASELEVEL_ATTR = SCOPE + "_BaseLevel";
// get current state from request - defaults are level=0, and isFirst=true
int level = (request.getAttribute(LEVEL_ATTR) == null ? 0 : Integer.parseInt((String)request.getAttribute(LEVEL_ATTR)));
boolean isFirst = (request.getAttribute(ISFIRST_ATTR) == null);
// find current level
RenderingContext rc = (RenderingContext) request.getAttribute(Workspace.WCM_RENDERINGCONTEXT_KEY);
Workspace ws = rc.getContent().getSourceWorkspace();
DocumentId docId = rc.getCurrentResultId();
String path = ws.getPathById(docId, false, false);
int currLevel = path.split("/").length;
// if going down to a deeper level or is first item, start a new list
if (currLevel > level || isFirst)
{
out.println("<ul class=\\"list\\">");
}
// if coming up from a deeper level, close off hanging list and containing item
else if (currLevel < level)
{
// close last item on previous level
out.println("</li>");
// close all the hanging lists/items
int levelsUp = level - currLevel;
for (int i = 0; i < levelsUp; i++)
{
out.println("</ul>");
out.println("</li>");
}
}
// if staying at same level, and not first item, close previous item
else if (currLevel == level && !isFirst)
{
out.println("</li>");
}
// check if this is an item on the current path
String currPath = ws.getPathById(rc.getContent().getId(), false, false);
boolean isSelected = currPath.startsWith(path);
// render list item tag, marking with styles for "first" and "selected"
out.print("<li class=\\"item" + (isFirst ? " first" : "") + (isSelected ? " selected" : "") + (docId.isOfType(DocumentTypes.Content) ? " content" : "") + "\\">");
// save current state
if (isFirst)
{
request.setAttribute(BASELEVEL_ATTR, Integer.toString(currLevel));
}
request.setAttribute(ISFIRST_ATTR, "false");
request.setAttribute(LEVEL_ATTR, Integer.toString(currLevel));
%>
Here is the second script - lets call this RenderNestedListCleanup.jsp - which is a cleanup script that is executed in the navigator's footer:
- this script does not access WCM data at all - it just reads the last state stored by the first JSP and closes up any hanging lists
- the script also cleans up the state attributes - this is done in case this is called again on this request; this may be unlikely, but better safe than sorry
<%@ page import="java.util.*,java.io.*,java.lang.*" %>
<%@taglib uri="/WEB-INF/tld/wcm.tld" prefix="wcm" %>
<%
String SCOPE = (String) request.getParameter("scope");
String LEVEL_ATTR = SCOPE + "_Level";
String ISFIRST_ATTR = SCOPE + "_IsFirst";
String BASELEVEL_ATTR = SCOPE + "_BaseLevel";
// get current state from request - defaults are level=0, baseLevel=0
int level = (request.getAttribute(LEVEL_ATTR) == null ? 0 : Integer.parseInt((String)request.getAttribute(LEVEL_ATTR)));
int baseLevel = (request.getAttribute(BASELEVEL_ATTR) == null ? 0 : Integer.parseInt((String)request.getAttribute(BASELEVEL_ATTR)));
// close last item
out.println("</li>");
// close hanging lists
int levelsUp = level - baseLevel; // note: will be zero if finished at the base level
for (int i = 0; i < levelsUp - 1; i++);
{
out.println("</ul>");
out.println("</li>");
}
// close off first list
out.println("</ul>");
// clear state
request.removeAttribute(ISFIRST_ATTR);
request.removeAttribute(LEVEL_ATTR);
request.removeAttribute(BASELEVEL_ATTR);
%>
To put it all together we need to create two JSP components in WCM:
- the path will be wherever you put the scripts (ideally in a different web application)
- the scope string being passed in to both scripts (note it is the same string in both) is there to avoid possible naming conflicts with other request attributes
Name: Render Nested List Result
Path: RenderNestedListResult.jsp?scope=myNav
Name: Render Nested List Cleanup
Path: RenderNestedListCleanup.jsp?scope=myNav
And a navigator component:
- this example is an auto expanding navigator starting at the Site - it could have started at a selected start area instead, and it could have been pre-expanded too; it does not matter to the script
- the script was written for top down navigation like the example below, but it *should* work with any navigator, including those that show descendent levels or siblings
- I used a single result design as all levels in my nav have the same data - you can use multiple result designs but you *must* ensure each one executes the "Render Nested List Result " script
- please note the 'compute="always"' option on the reference to the JSP component - this is necessary for the script to work as it must be invoked on each result
- I haven't included the necessary markup for accessibility - I'll leave that up to you!
Name: myNav
Start type: Site
Include Start: false
Ancestor level: None
Descendent level: 1
Preceding Siblings level: None
Next Siblings level: None
Show site: false
Show content: true
Expand current navigator branch one level: true
Expand navigator to display current site area: true
Results per page: 50
Start page: 1
Maximum pages to include: 1
Pages to read ahead: 1
Header: <div class="myNav">
Footer: <component name="Render Nested List Cleanup"/></div>
Separator: none
Navigator result design 1: <component name="Render Nested List Result" compute="always"/><placeholder tag="titlelink"/>
No result design: <div class="myNav">No sitemap</div>
Conclusion
So there it is, with less than 50 lines of actual code we've got a generic solution that can be used on any navigation.
There is no parsing, no rewriting of markup, the navigator cache is utilised, and the output can be cached anywhere in the rendering pipleline.
It is also worth noting that the same algorithm, with minor changes to the tags being generated, could be used to create any sort of nested list of content with arbitrary XML tags.
This may be a useful integration point with other systems that require a digest to be generated out of WCM.