Tutorial: Advanced Navigation in Liferay Themes

This is part 1 of a series written for UI-dev/FED new to Liferay. This assumes you’re solid with css/html/javascript, and have at least a passing understanding of dynamic languages like PHP or ASP. If you have a Java background, you may want to skip the parts that explain stuff you already know.

When you first open a basic Liferay template like portal_normal.vm, the navigation’s usually already set up for you. It’ll either be the parent-pages (top level) only, or it might have a single layer of children. It’ll probably look like this.

<nav class="$nav_css_class main_nav" id="navigation">
 <ul>
 #foreach ($nav_item in $nav_items)
 <li><a href="$nav_item.getURL()" $nav_item.getTarget()>
 $nav_item.icon()$nav_item.getName()
 </a>
 #if ($nav_item.hasChildren())
 <ul >
 #foreach ($nav_child in $nav_item.getChildren())
 <li><a href="$nav_child.getURL()" $nav_child.getTarget()>
 $nav_child.getName()
 </a></li>
 #end
 </ul>
 #end
 </li>
 #end
 </ul>
 </nav>

Unfortunately, between modern site design and byzantine site architecture, the main navigation might end up with a lot more moving parts. In this post we’re going to tackle a site with complex architecture to demonstrate some of the ways you can whip a header menu into shape. For instance, let’s say your site’s architecture looks something like this.

navigation

There are four main areas (Solutions, Services, Products, and Support), with at least three child-pages for each, and another 2-3 child-child-pages. (I expanded a few to show the pattern used for the mockup pages.) In Solutions Group C, there’s an additional level of children. This is a bit much to put on the standard horizontal menu bar, and it might even end up too much for a vertical menu. It’s time to try a cuppa mega-menu.

First, to get a few ideas, go take a look at a few mega menus in the wild. There’s T-Mobile’s header, impressivewebs’ mega-menu with ajax,  and CodeCanyon’s mega-menu script. Since we’re just aiming for demo-level and I like the sliding action better than T-Mobile’s fadeIn version, I’m going to mimic the CodeCanyon version using the Anything Slider. You can get the Anything Slider from GitHub.

Normally a multi-level menu follows the site architecture pretty closely. If you’re familiar with Tutsplus’ CSS3 Mega Menu (http://net.tutsplus.com/tutorials/html-css-techniques/how-to-build-a-kick-butt-css3-mega-drop-down-menu/) you’ll see the children pages are basically unordered lists embedded within the list items of the parent-list. Using the Anything Slider means we need to divorce the parents from the children, and that means a few extra steps in the navigation template.

Start with the usual calls to jQuery 1.9, jQuery ui, jQuery easing, and the Anything Slider. In main.js, skip all the AUI parts and start at the bottom of the file to insert your call to the slider.

$(function() {
 $('#slider1').anythingSlider({
 buildNavigation: false,
 infiniteSlides: false
 });
 }

Next step is a bit of CSS, since within each slide we’ll want a column for each child-page and its child-child-pages. It looks like no one parent-page has more than five columns, so here’s a bit of css to save you the time.

.column5 {
 margin: 0 2%;
 padding: 0;
 width: 16%;
 display: inline;
 float: left;
 text-align: left;
 }
.column5 ul { margin: 0; padding: 0; }
.column5 ul li {
list-style: square;
list-style-position:inside;
color: #888;
margin: 0 0 .25em 0;
}

Velocity lets you do includes, so it’s up to you if you want portal_normal.vm to be filled up with all the structure or if you want it neat and clean by including other .vm files. I’ll split the menu-parts between the two to demonstrate.

Onto the main menu structure. If you make the parent-headers dynamic, you could throw in an extra loop for that (based on the simple loop above), but there’s a gotcha here. The first instance in the loop, the “Welcome” page, has no children. If you include it in the loop, you’ll end up with an empty slide. There’s a way to do this dynamically, but in this example it’s just one slide to skip, we’ll hard-code it. The first instance gets an href of “home” and the next four get the slide-triggering hrefs of a hash-plus-number.

<header id="banner" role="banner">
 <div id="topofpage">
 <ul class="menu_mainrow" id="headerNav">
 <li class="site-title">
 <a class="$logo_css_class" href="home" title="#language("go-to") $site_name">
 <img alt="$logo_description" src="$site_logo" />
 </a>
 </li>
 <li class=" menu_option"><a href="#2">Solutions</a></li>
 <li class=" menu_option"><a href="#3">Services</a></li>
 <li class=" menu_option"><a href="#4">Products</a></li>
 <li class=" menu_option"><a href="#5">Support</a></li>
 </ul>
 #if ($has_navigation)
 #parse ("$full_templates_path/navigation.vm")
 #end
 </div>
 </header>

Warning: AUI sometimes injects things for its own needs, so you’ll want to be very careful around anything that’s a common HTML classname or element. When the browser crashes because it’s trying to do three things at once, you’ve run into conflicts in the namespace. It’s easiest to just avoid that issue and come up with alternate classes, like “topofpage”, and work with those instead.

Now open navigation.vm, which is where the rest of the menu navigation will reside. If every slide gets the same treatment, then it’s pretty simple. You can just loop through the children as many layers deep as needed:

<nav class="$nav_css_class scrollcontainer" id="navigation">
 <ul id="slider1">
 #foreach ($nav_item in $nav_items)
 #if ($nav_item.hasChildren())
 #foreach ($nav_child in $nav_item.getChildren())
 <div class="column5">
 <h4><a href="$nav_child.getURL()" $nav_child.getTarget()>
 $nav_child.getName()
 </a></h4>
 #if ($nav_child.hasChildren())
 <ul class="columnlist">
 #foreach ($nav_child in $nav_child.getChildren())
 <li><span>
 <a href="$nav_child.getURL()" $nav_child.getTarget()>
 $nav_child.getName()
 </a>
 </span></li>
 #end
 </ul>
 #end
 </div>
 #end
 #end
 #end
 </li>
 #end
 </ul>
 </nav>

Note that between line 3 and line 4 is where you’d normally put your bit for the parent-pages (ie page-names as <li>). Since we’ve got that covered already, we can skip that and go right to the children. Another important detail is the way Velocity loops with parents and children. The first time you loop, you’ll be dealing with $nav_item, which means using a pattern like $nav_item.getURL(). The second loop will use $nav_child, so you’ll need to use $nav_child.getURL() instead. Once you’re past those first two loops, any additional loops will just be $nav_child in $nav_child.getChildren() all the way down.

Just remember to close out every #if or #foreach with an #end.

But let’s say that unfortunately for you, the Information Architect and the Interface Designer have conspired to turn you into a weeping mass in the corner. They’ve declared they want the column contents to be very exact.  If you look back up at the site architecture, there are actually two columns for Business Services, two for Web Applications, and even an extra column called “Additional Products cont”. You can’t just loop through a 50% width <li> on a float:left, and it’s pretty clunky to count the <li> and then divide by two, especially if the decree is to have specific items in specific columns. That means we need to get rid of those titles that contain “cont” and make  the second column look like a continuation of the first. We’ll do that by comparing the name-values.  One way is to replace the <h4> section with this:

<h4>
 #if (($nav_child.getName() != " Business Services cont")
 && ($nav_child.getName() !=  " Web Applications cont" )
 && ($nav_child.getName() != "Additional Products cont"))
 <a href="$nav_child.getURL()" $nav_child.getTarget()>$nav_child.getName()</a>
 #end
 </h4>

(Normally I’d have that #if line all one line, but I’m forcing wraps where I can, just for this demo.)

Alternately, you could check to see if the name contains a value, like “cont”. As long as you don’t have any h4-level page-names with “cont” in them (like “contact” or “contestant” or “contents”), you should be fine. Then again, you could probably use whatever you like (ie “no-display” or “hidethis”), since this isn’t a page-name that will show up anywhere.

This might seem like a case for marking a page as hidden, but that’ll just remove it – and all its children– from the navigation completely. We do want the children to show up; we just don’t want the children’s parent-name to display.

For this alternate method, we’ll set a var and see if the $nav_child.getName() contains that var. I’ve switched the if/else results, since now we’re finding pages that DO contain “cont”, instead of finding pages that do NOT.

<h4>
 #set ( $pagename = (($nav_child.getName() )
 #set ( $hidepage = “cont” )
 #if ( $ pagename.contains($hidepage) )
 &nbsp;
 #else
 <a href="$nav_child.getURL()" $nav_child.getTarget()>
 $nav_child.getName()
 </a>
 #end
 </h4>

When you set var in Velocity, they’ve got to be inside paren. If you forget the paren, your <h4> will suddenly look like this:

#set $pagename = Web Application con t #set $hidepage = “cont”

…which is what Velocity does when it doesn’t know, or doesn’t have, a var value. It’ll just spit the text out at you along with the $.

For the final gotcha, there’s the additional child-level in the Solutions sub-pages. That’s the only instance, and let’s say that’s one more way the designers have conspired against you. That first slide needs a completely different set of styles, which means not only do you need to call them out so you can apply a different class and loop through one more level, you also need to make sure they don’t get included (repeated) in the loops for the other slides.

We’ll be following roughly the same pattern as above, but this time at the top-most slide level (the first set of children under the parent-level). You can tell it’s the second level because we’re looking for $nav_item’s children, not $nav_child’s children.

#if ($nav_item.hasChildren())
 #foreach ($nav_child in $nav_item.getChildren())
 #if ($nav_child.getName() == "Solutions Group A")
 <div class="thispanel_tab">
 <h4>$nav_child.getName()</h4>
 <ul class="columnlist ">
 #if ($nav_child.hasChildren())
 #foreach ($nav_child in $nav_child.getChildren())
 <li><span>
 <a href="$nav_child.getURL()" $nav_child.getTarget()>
 $nav_child.getName()
 </a>
 </span></li>
 #end
 #end
 </ul>
 </div>
 #elseif ($nav_child.getName() == "Solutions Group B")
 // another loop or two in here for the children
     #elseif ($nav_child.getName() == "Solutions Group C")
 // another loop or two in here for the children
 #end

You could do a contains on this loop, if you’re lucky enough to have page titles that will always contain a consistent word or set of words (ie “Solutions Group”). If not, you’ll need to go through each one separately. You’ll then need to do a second loop to get all the not-instances. Velocity can get finicky about the #else. If it does, then just do a second full navigation loop and kick it off with this line:

#if (($nav_child.getName() != "Solutions Group A")
 && ($nav_child.getName() !=  "Solutions Group B" ) ...

…etc.

That’ll knock out all the pages you called out in the first navigation loop. Now you’ve got a completely different set of classnames associated with the elements in the first slide, and the other three slides are all repeating classnames. If you’re lucky enough to have a consistent page-naming format, you can look for the unique word or words that match the pages you want called out. Here’s what it’ll look like, all put together.

<nav class="$nav_css_class scrollcontainer" id="navigation">
 <ul id="slider">
 #foreach ($nav_item in $nav_items)
 <li class="thispanel">
 #if ($nav_item.hasChildren())
 #foreach ($nav_child in $nav_item.getChildren())
 #set ( $navchild_name = $nav_child.getName() )
 #set ( $solutionspage = "Solutions Group" )
 #if ($navchild_name.contains($solutionspage))
 <div class="thispanel_tab">
 <h4>$nav_child.getName()</h4>
 <ul class="columnlist_swing">
 #if ($nav_child.hasChildren())
 #foreach ($nav_child in $nav_child.getChildren())
 <li><span>
 <a href="$nav_child.getURL()" $nav_child.getTarget()
 >$nav_child.getName()
 </a></span></li>
 #end
 #end
 </ul>
 </div>
 #else
 <div class="column5">
 <h4>
 #set ( $pagename = $nav_child.getName() )
 #set ( $hidepage = "cont" )
 #if ($pagename.contains($hidepage))
 &nbsp;
 #else
 <a href="$nav_child.getURL()" $nav_child.getTarget()>
 $nav_child.getName()
 </a>
 #end
 </h4>
 #if ($nav_child.hasChildren())
 <ul class="columnlist">
 #foreach ($nav_child in $nav_child.getChildren())
 <li><span>
 <a href="$nav_child.getURL()" $nav_child.getTarget()>
 $nav_child.getName()
 </a></span></li>
 #end
 </ul>
 #end
 </div>
 #end
 #end
 #end
 </li>
 #end
 </ul>
 </nav>

The final version will look something like this (note this is styled very simply for the tutorial and in a “real” implementation would be styled to look a whole lot nicer)

megamenu

 

In  my next post, I’ll cover a few methods for dealing with menus that are independent of the site’s formal structure.

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Post Comment