HTML::Navigation - generic HTML navigation structure class
my $nav = new HTML::Navigation();
# a simple, one-level menu $nav->structure([ __callbacks__ => [ $callbacks ], __param__ => 'param1', 'foo', 'bar', ]);
# output a two-item navigation menu with the `bar' item selected print $nav->output({ param1 => 'bar' });
HTML::Navigation makes it easy to generate an HTML navigation structure without forcing you into any particular layout or design. All the output is done by your own subroutines, which the module invokes as callbacks (code refs).
You supply the navigation structure and callbacks for generating different bits of the output, and the module takes care of the rest. You may wonder what ``the rest'' refers to; what else there is to do except generate output? Well, HTML::Navigation really comes into its own with nested (multi-level) navigation structures, including dynamically generated ones. The structures are ordered directed acyclic graphs where each node is a menu item which can optionally have subnodes.
See TUTORIAL for the quickest way to learn how the module works.
Please note that parsing of the structure is currently performed when
output()
is called rather than when new()
is
called. This avoids unnecessary calls to potentially expensive callbacks
which may be given to dynamically generate parts of the navigation
structure.
my $nav = new HTML::Navigation(structure => $structure);
or
my $nav = new HTML::Navigation(); $nav->structure($structure);
The two forms are identical in results. Likewise you can specify a
base_url
parameter instead of setting a base url via the
base_url()
method.
my $structure = $nav->structure();
$nav->structure($new_structure);
Reads/writes the navigation data structure. See TUTORIAL for an extensive tutorial on how to form these data structures.
my %CGI_params = map { $_ => $req->param($_) } qw/param1 param2/; my $out = $nav->output(\%CGI_params);
Returns the HTML for a navigation menu, as defined by the
_navigation
key in the frontend object.
Returns an array containing all the combinations of CGI parameters required to select every element in the navigation structure. Each combination is in standard CGI QUERY_STRING format, i.e.
param1=foo;param2=bar;param3=...
sub unselected_callback { my ($nav, %p) = @_;
# make unselected menu item a hyperlink return $nav->ahref(text => $p{item}, params => [ $nav->params(%p) ]); }
Helper method for figuring out what CGI parameters are needed to point to
the item described by %p
.
my $html = ahref(url => 'http://www.foobar.com/baz.pl', text => 'click me', # defaults to the url parameter params => { param1 => 'val1', param2 => 'val2', }, # any other parameters get added as attributes, e.g. class => 'myclass', # <A ... CLASS="myclass" ...> ... );
Convenient method for generating hyperlinks. All parameters are optional,
but ahref()
will moan if it can't figure out a sensible value
for HREF. The value for the params
key can be a hashref or an arrayref; in the latter case the order of the
parameters is preserved in the output.
# Set $query_string to `param1=foo%20bar;param2=baz' my $query_string = $nav->query_string([ param1 => 'foo bar', param2 => 'baz', ]);
This method provides an easy way of generating a string suitable for appending to the end of a CGI URL in order to create GET queries.
You can pass the parameters in either a hashref or an arrayref; in the latter case the order given is preserved.
my $base_url = $nav->base_url(); $nav->base_url('foo.pl');
Read/write a base url for the links generated by ahref()
to default to if no url
parameter is given.
my $current_level = $self->debug_level();
$self->debug_level($new_level);
Read/write debugging verbosity level (defaults to 0).
Debugging appears on STDOUT.
Here are some examples of navigation structures. I will try to introduce
the concepts in increasing order of complexity. The structures are always
suitable for passing to the structure()
method, and each one can be found as part of a fully working CGI script in
the `eg' directory so that you can experiment with them more yourself.
Note that they are always arrayrefs rather than hashrefs so that the ordering of the items is preserved.
This structure describes a single-level (no submenus) menu with three items. The value following `__param__' is the CGI parameter used to determine which item is selected, and in this case is innovatively called `param'.
# Extract from eg/single.cgi [ __param__ => 'param', __callbacks__ => [{ pre_items => sub { "<ol>\n" }, post_items => sub { "</ol>\n" }, pre_item => sub { " <li> " }, post_item => sub { "\n" }, unselected => sub { my ($nav, %p) = @_; return $nav->ahref(text => $p{item}, params => [ $nav->params(%p) ]); }, selected => sub { my ($nav, %p) = @_; return $p{item}; }, }], 'item 1', 'item 2', 'item 3', ]
The value following __callbacks__ is an arrayref containing one hashref for each level of the navigation structure. The example above has only one level, so there is only one hashref inside the array ref. The hashrefs map types of callback to the callbacks themselves, which generate all the output. So in this example, the callbacks get invoked in the following order:
pre_items pre_item for item 1 selected or unselected for item 1 post_item for item 1
pre_item for item 2 selected or unselected for item 2 post_item for item 2
pre_item for item 3 selected or unselected for item 3 post_item for item 3 post_items
Output occurs when the output()
method is called, which takes as its sole argument a hashref describing the
current CGI parameters. It uses this to determine whether an item is
selected or not, so if it is called as
$nav->output({ param => 'item 2' });
then the callbacks will be invoked as follows:
pre_items pre_item for item 1 unselected for item 1 post_item for item 1
pre_item for item 2 selected for item 2 post_item for item 2
pre_item for item 3 unselected for item 3 post_item for item 3 post_items
As you can see from the `unselected' and `selected' callbacks in the above code, when a callback is invoked, it gets passed the HTML::Navigation object as the first parameter, and the remaining parameters form a hash which contains all the information you could possibly need to know about the current item in order to generate suitable output for it. The keys for the hash include:
The name of the current item, e.g. `item 2'.
The depth of the current item in the navigation graph. This will always be 0 until we progress to the multi-level examples below.
True iff the current item is selected. Normally you won't need this because you know whether it's selected depending on whether the `selected' or `unselected' callback has been invoked, but you could use this to change the behaviour of the pre_item callback depending on whether the item is selected, for example.
True iff the current item is a leaf in the navigation tree. It will always be a leaf unless it's a submenu which is currently selected.
The name of the current item's parent. This is `top' if we're at level 0 (which, as noted above, has always been the case in the examples so far).
You should not attempt to change any of the values in this hash. If you do so, you invalidate the module's warranty and no guarantees about its behaviour can be made.
There are two more callback types to know about. The first is `item_glue', which is invoked in between each item:
pre_items pre_item for item 1 selected or unselected for item 1 post_item for item 1
item_glue
pre_item for item 2 selected or unselected for item 2 post_item for item 2
item_glue
pre_item for item 3 selected or unselected for item 3 post_item for item 3 post_items
The second is `omit', which doesn't generate any output, but decides whether a particular item should be included in or omitted from the output. Say that we used the following omit callback:
sub { my ($nav, %p) = @_; return $p{item} eq 'item 2'; }
Then the callback order would be:
pre_items pre_item for item 1 selected or unselected for item 1 post_item for item 1
item_glue
pre_item for item 3 selected or unselected for item 3 post_item for item 3 post_items
If we generate output for our single-level menu with no item selected like so:
$nav->output({});
then the callback invocation order will be
pre_items pre_item for item 1 unselected for item 1 post_item for item 1
pre_item for item 2 unselected for item 2 post_item for item 2
pre_item for item 3 unselected for item 3 post_item for item 3 post_items
so that no item appears selected. But what if we always want an item
selected, even when the CGI parameter to select one is missing? The answer
is to include __default__
in the structure:
[ __param__ => 'param', __callbacks__ => [ $level_0_callbacks ], __default__ => 'item 2', 'item 1', 'item 2', 'item 3', ]
Now if the CGI parameter `param' is missing, `item 2' will be selected. If
you want the default selected item to be the first item in the list, but
you don't necessarily know what the first item is called (see Dynamically generating items below) then set
__default__
to the empty string.
If you want, you can dynamically build up the navigation structure at output time by using coderefs:
# Extract from eg/dynamic.cgi sub dynamic_items { [ 'item 2', 'item 3' ] }
my $nav = new HTML::Navigation(base_url => 'simple.cgi'); my $structure = [ __param__ => 'param', __callbacks__ => [{ pre_items => sub { "<ol>\n" }, post_items => sub { "</ol>\n" }, pre_item => sub { " <li> " }, post_item => sub { "\n" }, unselected => sub { my ($nav, %p) = @_; return $nav->ahref(text => $p{item}, params => [ $nav->params(%p) ]); }, selected => sub { my ($nav, %p) = @_; return $p{item}; }, 'item 1', \&dynamic_items, ];
dynamic_items()
will be invoked during the call to output()
, not before. This is mostly of use with multi-level navigation, for which
see below.
You can use this ``unpacking coderefs'' technique to dynamically generate
as much of the contents of the containing arrayref as you want, i.e. even
the __param__
, __callbacks__
, and __default__
bits.
You now know everything about single-level navigation!
Again, this is best illustrated with an example of a two-level menu (eg/two-level.cgi).
# Extract from eg/two-level.cgi [ __param__ => 'first', __callbacks__ => [ # level 0 { pre_items => sub { "<ol>\n" }, post_items => sub { "</ol>\n" }, pre_item => sub { "<li> " }, post_item => sub { "\n" }, unselected => sub { my ($nav, %p) = @_; return $nav->ahref(text => $p{item}, params => [ $nav->params(%p) ]); }, selected => sub { my ($nav, %p) = @_; return $p{item}; }, },
# level 1 { pre_items => sub { "<ul>\n" }, post_items => sub { "</ul>\n" }, pre_item => sub { "<li> " }, }, ], 'item 1' => [ __param__ => 'submenu_1', 'one', 'two', 'three', ], 'item 2', 'item 3' => [ __param__ => 'submenu_2', __default__ => 'five', __callbacks__ => [{ pre_item => sub { "<li> <b>" }, post_item => sub { " </b>\n" }, }], 'four', 'five', 'six', 'seven', ], ]
This has a top-level menu as before, but now clicking on the first and
third items reveal further submenus containing `one' to `three', and `four'
to `seven' respectively. Note the difference that
__default__
creates between the two submenus: when you click on `item 1' it reveals the
first submenu but none of the sub-items are selected, whereas when you
click on `item 3' then `five' immediately gets selected.
Also note how the callbacks are defined for the submenus (level 1). Firstly the `pre_items', `post_items', `pre_item', `post_item', `unselected', and `selected' callbacks are inherited from level 0. Then the `pre_items', `post_items', and `pre_item' callbacks are overriden by the callbacks in the hashref following the `# level 1' comment. Finally, in the `item 3' submenu only, the `pre_item' and `post_item' callbacks are overriden. The net effect of all this is that both submenus are unordered lists, and the items of the second submenu (`four' to `seven') are in bold.
Finally, note that although in this example each submenu has a different CGI parameter name determining selection within it (`submenu_1' and `submenu_2'), everything would still work if they had the same CGI parameter name (e.g. `submenu').
Dynamic item generation works as before, except it is now potentially much more useful, because if you have a submenu whose contains are generated by a coderef which is an expensive operation (e.g. doing a complex query on a database), then that expensive operation will only be performed if the contents of that submenu are visible (i.e. iff the submenu has been selected).
All aspects of the navigation structure syntax have now been covered. If you really want to, you could take a look at the multi-level structure in t/MyTest.pm, which contains some pathological features designed to test the code to its limits.
Here are a few things aren't nice, and a few things which might be. Suggestions and comments welcome.
The recursion code is convoluted and should be abstracted out.
The dump_all_params()
stuff is a nasty hack, which I only
included to make testing easier.
Some of the subroutines are way too long. I tried breaking them up several times but always ended up with something even messier :-(
Maybe it would be cleaner to do output in two passes - one for parsing the navigation structure, and one for doing the output. I'm guessing that most (all?) navigation structures won't be big enough to worry about the cost of doing two passes, but I could be wrong.
Recursion is currently depth-first only.
There needs to be a sanity check for duplicate __param__
values at different levels.
Creation/manipulation/retrieval of specific bits of the navigation structure via methods, e.g.
$nav->top->submenu('foo')->add_item('item under foo submenu');
Maybe use Tree::DAG_Node? This would mean some major changes though, as the structure is currently described as an arrayref, not a hashref, so as to preserve ordering. If the structure parsing phase was separated out, that would make this a lot easier.
Optional tree-parsing during new()
phase rather than during
output()
phase. This has to be optional, because it would mean
that any coderefs given to dynamically generate menu items would have to be
run here, which is bad if your coderefs point to expensive code.
Debugging isn't tested. But then that's kind of the point.
Adam Spiers <adam@spiers.net>