Wednesday, December 19, 2007

Suckerfish dropdowns for Wicket (redux)

I originally contributed the code from my original post to Wicket Stuff as the projects wicketstuff-suckerfish and wicketstuff-suckerfish-examples. However, those projects are now gone, so here is a link to the code.

I've made some improvements to the original code. Dropdowns are built hierarchically from Wicket Fragments so in theory there's no limit to how many levels deep you can go; and associating a link with any menu item is optional, so for example you can have a top-level menu bar with no links, just labels. I've tested it with IE6, IE7 and FF2.

This project shows how useful Wicket Fragments can be. My original post didn't use fragments and was limited to one level of dropdowns. Using fragments kept the code short and allows an unlimited number of levels of dropdowns (although the CSS will need to be improved to go beyond two levels).

An example to show the second improvement (a top-level menu bar with no links, just labels, taken from the code in wicketstuff-suckerfish-examples):
// Create the menubar
final SuckerfishMenuPanel mb = new SuckerfishMenuPanel("menuBar");
add(mb);

// Create a menu. Clicking on the menu itself will take you to the home page
final SuckerfishMenuPanel.MenuItem mi = new SuckerfishMenuPanel.MenuItem(
new BookmarkablePageLink(SuckerfishMenuPanel.LINK_ID,
HomePage.class), "Home");
mb.addMenu(mi);

// Create a second menu without a link to the home page
final SuckerfishMenuPanel.MenuItem mi2 = new SuckerfishMenuPanel.MenuItem(
"Home");
mb.addMenu(mi2);


Discussion of the code (from wicketstuff-suckerfish):

From SuckerfishMenuPanel.html:
The menu bar is an unordered list of menu item fragments:
<ul id="nav">
<li id="topmenuitems">
<span id="menuitemfragment">link</span>
</li>
</ul>


A menu item fragment contains a link fragment and an unordered list of menu item fragments:
<wicket:fragment id="MENUITEMFRAGMENT">
<span id="linkfragment">link</span>
<ul id="menuitemlist">
<li id="menuitemlinks">
<span id="menuitemfragment">link</span>
</li>
</ul>
</wicket:fragment>


A "link fragment" can in fact be one of two types of fragments, because the link part is optional: one with a hyperlink, and one consisting just of text. The following shows the type with a hyperlink. It consists of an anchor that embeds a span:
<wicket:fragment id="LINKFRAGMENT">
<a href="#" id="linkid" class="childmenu">
<span id="linktext" class="childmenutext">Link Text</span>
</a>
</wicket:fragment>


A text fragment is a degenerate link fragment without the hyperlink:

<wicket:fragment id="TEXTFRAGMENT">
<span id="linktext" class="childmenutext">Link Text</span>
</wicket:fragment>


This structure allows you to embed menu item fragments to any depth.

Here's the code for MenuItemFragment. It takes a MenuItem as a constructor argument, and depending on whether a Link is provided, it constructs a LinkFragment or a TextFragment. It then recursively creates more MenuItemFragments by passing the list of children of the MenuItem to a SubMenuListView.

One standard Wicket trick to note is the use of a WebMarkupContainer. This is used for the UL tag, for which there is no standard Wicket component. We need it so that we can optionally hide it if the UL has no children. You might ask, why construct it only to then hide it? That's the way Wicket works. If it's referred to in the html, it must be constructed.
private final class MenuItemFragment extends Fragment
{
private static final long serialVersionUID = 0L;

public MenuItemFragment(MenuItem menuItem)
{
super("menuitemfragment", "MENUITEMFRAGMENT",
SuckerfishMenuPanel.this);
// Add the menu's label (hyperlinked if a link is provided)
if (menuItem.getLink() != null)
{
add(new LinkFragment(menuItem.getLink(), menuItem.getLabel()));
}
else
{
add(new TextFragment(menuItem.getLabel()));
}
final WebMarkupContainer menuitemul = new WebMarkupContainer(
"menuitemlist");
add(menuitemul);
// Hide the <ul> tag if there are no submenus
menuitemul.setVisible(menuItem.getChildren().size() > 0);
// Add the submenus
menuitemul.add(new SubMenuListView("menuitemlinks", menuItem
.getChildren()));
}
}

15 comments:

Suresh Byanjankar said...

Nice work Julian. You have done exactly what I am looking for. I tried your example and it worked well in navigation. But I got a problem in it. One of my menus is named as AddUser which contains the form to add new user. When I fill the form and try to get the feedback after form submission I got the following error
java.lang.IllegalArgumentException: A child with id 'linktext' already exists:

Could you please test with the form in one of the menus and get feedback message (e.g for form validation) without any error and inform me if it works. Thanks

Julian Sinai said...

Suresh, I've fixed this problem. Please try it again.

Suresh Byanjankar said...

Thanks Julian it works now. I changed the Link to External Link in your code. Now I can generate dynamic links as well in the drop down menus.

Eyal said...

Hello,
Works great!! Thanks.
I have a question regarding IE.
when I roll out from a menu / submenu, it stays on.
On FF I don't have this problem.

Can you help?

Thanks,
Eyal

Julian Sinai said...

Eyal, I don't see this problem on IE. Please double check that your javascript onmouseout method matches mine.

martin-g said...

Great work Julian,

I want to propose a RFE (for the wicketstuff code): please change all
occurences of "Link" to "AbstractLink" so the code could be easily used with Ajax/External links

Eyal said...

Julian,
The problem was when the menu was inside an iframe.
That was only for testings.
When I put the menu in the page itself, it works great.

Small question regarding styling.
I happen to have many items in the menu and it gets two lines.
How can I make the width of each item to be the size of the text + some pixels?
I'm not an expert in CSS and wonder if you can give me this shortcut :)

Thanks

Julian Sinai said...

Martin, I've made the change to AbstractLink and checked it in. However, ExternalLink does not inherit from AbstractLink so your comment isn't accurate.

Julian Sinai said...

Eyal, I don't know of a way to have the text be just the width of the text plus some extra space. However, by playing around with the CSS I was able to reduce the width in my example. See the parameters that are 7em below.

#nav span.childmenutext {
display: block;
width: 7em;
padding: 0.25em;
cursor: default;
}

#nav li ul {
position: absolute;
left: -999em;
height: auto;
width: 7em;
font-weight: normal;
margin: 0;
}

#nav li li {
padding-right: 1em;
width: 7em;
padding-left: 0;
padding-top: 0.25em;
padding-bottom: 0.25em;
}

#nav li ul a {
width: 7em;
}

#nav li ul ul {
margin: -1.75em 0 0 7em;
}

martin-g said...

Hi Julian,

Thanks for the update !

About my comment accuracy. See:
http://svn.apache.org/viewvc/wicket/trunk/jdk-1.4/wicket/src/main/java/org/apache/wicket/markup/html/link/ExternalLink.java?view=markup

Probably you have looked at http://people.apache.org/~tobrien/wicket/apidocs/index.html
Here by some reason it says that the parent is WebMarkupContainer...

Julian Sinai said...

Martin, you are correct regarding ExternalLink--the javadoc does not match the actual code. ExternalLink does in fact inherit from AbstractLink. I based my comment on the javadoc.

eneve said...

Not sure if this is still the right place to be commenting on this but...

I want a different style on the top level li elements than the rest of the submenus (-moz-border-radius: 5px).

Also, I want the second half of the top level li to have their submenus pop out to the left instead of the right so that they display without scrolling off the screen.

I was thinking that SuckerfishMenuPanel.MenuItem should extends Component that way you can add SimpleAttributeModifier etc. to the menu items.

Is this the correct approach? Thx!

Timaar said...

Is it possible to repost the code?

The links are not available

Julian Sinai said...

Timaar, here is a link to the code: https://docs.google.com/file/d/0B917-nAEp_TkTTJScS1lYTlSQ1NiNjQ2ZDRCRDI5dw/edit

Timaar said...

Thanks Julian but to get access to the google docs I think you need to either set access to everyone or approve my gmail account to get access.


I am learning wicket and I am breaking my head over this. I could use a working example of this. So if you could give me access to the code that would be much appreciated!