Providing Link-Based Navigation

only for RuBoard - do not distribute or recompile

Near the beginning of the chapter, I mentioned that link-based navigation has certain limitations that search forms circumvent by enabling you to help your users find what they re looking for more directly. However, I also pointed out that link navigation can be useful sometimes, and this section discusses the kind of situation for which that is true.

My primary objection to link navigation earlier was that it enables users to move through your documents only in ways you can think of to provide in advance. If your document set has a regular and highly structured organization, however, you know perfectly well what the navigation options are, and it may be quite reasonable to provide links to all of them. If you have information about a list of people, for example, you can provide a nicely organized set of links that enable visitors to quickly get to any particular item in the list. If your information is stored in your database, that s even better; you can generate the links dynamically as people visit your site and they ll always be up-to-date without endless manual tweaking and editing when the information changes.

Let s apply this concept to a situation in which you want to maintain an online staff directory for your organization: Each person has an entry in a staff table in your database, and you want to make it easy to find any given person by name. To do this, we ll provide an index consisting of a set of links, with each link pointing to an individual staff table entry. The links will be sorted alphabetically so that they can be scanned easily. We ll also separate the index into sections by letter (all names beginning with A, all names beginning with B, and so forth), and provide a navigation bar listing the letters as links to each section of the index. Thus, to find the entry for Joe Smith, you select S from the navigation bar to get to the section of S names, scan that section to find Joe s name, and then select the name to see the information in Joe s staff table record.

We ll implement this directory two ways: once using a single-page index and once using a multiple-page index. The single-page index will provide a navigation bar and list the names of all staff members on a single page. The multiple-page index will consist of two tiers. The lower tier provides separate pages for each letter, with each page listing only names beginning with the corresponding letter. The upper tier is a single page that contains a navigation bar pointing to the lower-level pages.

These methods have in common the need to know how to present the entry for a given staff person (which is what your visitors actually are interested in seeing, after all), and they need to know how to construct indexes based on the contents of the staff table . Where they differ is in the particular form of the indexes (one-tier versus two-tier) and on the scope of the items listed on a page (all items versus items only for a given letter). The scope of the single-page index comprises all records, and therefore is suitable primarily for smaller record sets. Otherwise, you end up generating a page that is quite long, takes a long time to load, and is cumbersome to use. A multiple-page index accommodates larger numbers of items, because the scope of each subindex page is limited to just a part of the record set. This makes the individual pages easier to manage. It s also less work for the database and your application, because you retrieve only part of the directory for each subindex page.

To present the single-page and multiple-page versions of the staff directory, we ll write two applications, staffs.pl and staffm.pl. They both use the same underlying staff table, which looks like this:

 CREATE TABLE staff  (     id          INT UNSIGNED NOT NULL AUTO_INCREMENT,   # record identifier      PRIMARY KEY (id),      lname       VARCHAR(30) NOT NULL,                   # last name      fname       VARCHAR(30) NOT NULL,                   # first name      position    VARCHAR(30) NOT NULL                    # job  ) 

This table is quite minimal, containing only an id column that provides a unique record identifier, the first and last names, and staff position. It s simple because the purpose of this section is to show how to provide structured access to the table s records, not how to display them. Besides, the basic principles for displaying link-based index pages are quite similar for more complex forms of information, so you can adapt them to many different kinds of tables without much modification.

The general form of the queries used by both staffs.pl and staffm.pl are similar, so I ll describe them here before moving on to the specifics for each application. Each script needs to determine appropriate entries for the navigation bar, which means figuring out which letters are represented by records in the staff table. This can be done using the following query:

 SELECT DISTINCT UPPER(LEFT(lname,1)) AS letter FROM staff ORDER BY letter 

LEFT(lname,1) extracts the first letter of each staff member s last name, ORDER BY sorts them, and DISTINCT makes sure we get back each letter only once. You could just generate a set of 26 links and avoid running this query, but then you might end up with links that point nowhere. (There s no point in having a link for X if nobody has a last name beginning with X. ) The query enables us to avoid generating dead links in the navigation bar. Also, the query makes the navigation bar somewhat less language dependent (perhaps your alphabet doesn t have 26 letters).

To pull out names and format them for display in the single-page index, we can use a query of this form:

 SELECT id, CONCAT(lname,', ',fname) AS name FROM staff ORDER BY name 

For the multiple-page directory with a subindex page per letter, the query is similar but limited to entries for names beginning with a specific letter. For example, the following query retrieves names beginning with W :

 SELECT id, CONCAT(lname,', ',fname) AS name FROM staff  WHERE lname LIKE 'W%' ORDER BY name 

In both cases, the query also selects the id column. That value is needed to make the links in the item list point to the proper record associated with the name, because we ll be generating hyperlinks from the query results. The name becomes the label that s visible to the user in the index page, and the ID is used to construct a URL that invokes the application again and tells it to display a particular staff entry.

Presenting a Single-Page Directory

The script staffs.pl that presents the staff directory using a single page handles only two tasks: displaying the index page and displaying individual entries. The URLs for these two cases look like this:

 .../staffs.pl              present index page  .../staffs.pl?id=n         present record number n 

The main part of the program containing the dispatch logic is correspondingly simple:

 #! /usr/bin/perl -w  # staffs.pl - staff directory, single-page index  use strict;  use lib qw(/usr/local/apache/lib/perl);  use CGI qw(:standard :netscape escapeHTML);  use WebDB;  print header (), start_html (-title => "Staff Directory", -bgcolor => "white");  my $dbh = WebDB::connect ();  my $id = WebDB::trim (param ("id"));  if ($id ne "")                  # ID is present, display that entry  {     display_entry ($dbh, $id);  }  else                            # no ID given, display index page  {     display_index ($dbh);  }  $dbh->disconnect ();  print end_html ();  exit (0); 

Note that the use CGI statement includes the :netscape group. That s because the index page generation code uses the center() function, which is part of that group.

It s easier to format an individual entry for display than to construct the index (particularly because our staff table is so pathetically minimal!), so let s get that out of the way first. display_entry() takes arguments for the database handle and entry ID. It looks up the entry, prints its contents, and also provides a link back to the index page:

 sub display_entry  { my ($dbh, $id) = @_;  my ($sth, $ref);      $sth = $dbh->prepare ("SELECT * FROM staff WHERE id = ?");      $sth->execute ($id);      $ref = $sth->fetchrow_hashref ();      $sth->finish ();      if (!$ref)      {         print p (escapeHTML ("No record for ID $id was found."));          return;      }      # Display contents of record      print p (escapeHTML ("ID: $ref->{id}"));      print p (escapeHTML ("Name: $ref->{lname}, $ref->{fname}"));      print p (escapeHTML ("Position: $ref->{position}"));      # Display a link back to the index page      print hr (), p ("[" . a ({-href => url ()}, "index") . "]");  } 

Index page construction is the more interesting part. For a single-page index, the elements of the navigation bar point to other locations within the same page. The per-letter sections of the page to which the bar elements point can be marked with anchor tags of the following form, where c is a letter of the alphabet:

 <a name="c">c</a> 

To refer to these markers, the elements of the bar consist of hyperlinks like this:

 <a href="#c">c</a> 

The # character indicates a relative link to a location within the same page. The function gen_nav_bar() creates these links by determining the letters for which there are applicable staff table records and turning them into hyperlinks that point to each section of the index page:

 sub gen_nav_bar  { my $dbh = shift;  my ($letter_ref, @bar); 
     # Get reference to array of letters represented by staff entries      $letter_ref = $dbh->selectcol_arrayref (                         "SELECT DISTINCT UPPER(LEFT(lname,1)) AS letter                          FROM staff ORDER BY letter");      return () if !$letter_ref || !@{$letter_ref};   # error or empty result?      # Turn each letter into a relative link to the corresponding      # section of index page      return (map { "[" . a ({-href => "#$_"}, $_) . "]\n" } @{$letter_ref});  } 

The index page itself is generated by display_index(), which calls gen_nav_bar() to construct the navigation bar, and then displays it above and below the list of names:

 sub display_index  { my $dbh = shift;  my ($sth, @nav_bar, @item_list);  my $cur_letter = "";      @nav_bar = gen_nav_bar ($dbh);      if (!@nav_bar)      {         print p ("The directory has no entries.");          return;      }      print p (a ({-name => "top"},                  "Select a letter to go to that part of the directory:"));      print hr (), center (@nav_bar), hr ();      # Retrieve and display the list of items, grouped by initial letter      $sth = $dbh->prepare (                     "SELECT id, CONCAT(lname,', ',fname) AS name                      FROM staff ORDER BY name");      $sth->execute ();      while (my ($id, $name) = $sth->fetchrow_array ())      {         # If we've arrived at a new letter, begin a new list          if ($cur_letter ne uc (substr ($name, 0, 1)))          {             # Print any cached list for previous letter              print ul (li (\@item_list)) if @item_list;              # Set up for new list              @item_list = ();              $cur_letter = uc (substr ($name, 0, 1));              print p (strong (a ({-name => "$cur_letter"}, $cur_letter))                      . " [" . a ({-href => "#top"}, "index") . "]");          }          # Save current item in list          push (@item_list,                  a ({-href => url () . "?id=$id"}, escapeHTML ($name)));      }      $sth->finish ();      # Print final cached list      print ul (li (\@item_list)) if @item_list;      # Display navigation bar again      print hr (), center (@nav_bar), hr ();  } 

The entry list is displayed in sections, grouped by letter. Each section displays the letter as a caption, together with a link to the top of the page so that the user can get to the navigation bar easily. Within each section of the index, names are displayed using a bullet list.

Generating this kind of display requires us to notice while processing the entries when we begin each new section of the list, so that we can display appropriate caption letters at the beginning of each section. display_index() uses $cur_letter to track the section. Each time a mismatch occurs between $cur_letter and the initial letter of the name in the current record, we ve gotten to a new section of the index.

That s all there is to it. Use the cr_staff_tbl.sql script provided in the webdb distribution to create the staff table and populate it with some sample data. Then install staffs.pl in your script directory, request it from your browser, and click away to navigate through the staff directory.

Presenting a Multiple-Page Directory

The script staffm.pl presents the multiple-page version of the staff directory. It has to handle three tasks: generating the main index page, generating the subindex pages, and displaying individual entries.The URLs for these tasks look like this:

 .../staffm.pl                present main index page  .../staffm.pl?letter=c       present subindex page for letter c  .../staffm.pl?id=n           present record number n 

The main part of the application is quite similar to that of staffs.pl ; the only difference is that it also checks for a letter parameter and invokes display_subindex() if it s present:

 #! /usr/bin/perl -w  # staffm.pl - staff directory, multiple-page index  use strict;  use lib qw(/usr/local/apache/lib/perl);  use CGI qw(:standard :netscape escapeHTML);  use WebDB;  print header (), start_html (-title => "Staff Directory", -bgcolor => "white");  my $dbh = WebDB::connect ();  my $id = WebDB::trim (param ("id"));  my $letter = WebDB::trim (param ("letter"));  if ($id ne "")                  # ID is present, display that entry  {     display_entry ($dbh, $id);  }  elsif ($letter ne "")           # index letter is present, display subindex  {     display_subindex ($dbh, $letter);  }  else                            # no params given, display main index page  {     display_index ($dbh);  }  $dbh->disconnect ();  print end_html ();  exit (0); 

display_entry() is the same for staffm.pl as for staffs.pl ; no need to show it here. display_index(), on the other hand, is much simpler than the staffs.pl version. It needs only a navigation bar pointing to the subindex pages. You could pretty this page up a little with some descriptive text, but staffm.pl doesn t get too fancy in this regard. It just prints a short paragraph, the navigation bar, and a count indicating the number of entries in the directory:

 sub display_index  { my $dbh = shift;  my (@nav_bar, $count);      @nav_bar = gen_nav_bar ($dbh, "");      if (!@nav_bar)      {         print p ("The directory has no entries.");          return;      }      print p ("Welcome to the staff directory.\n"              . "Entries are listed by last name.\n"              . "Please select a letter below to find the person\n"              . "you're looking for.");      print hr (), center (@nav_bar), hr ();      # Show how many entries the directory contains      $count = $dbh->selectrow_array ("SELECT COUNT(*) FROM staff");      print p ("The directory contains $count entries.");  } 

The real work happens in display_subindex(), which, given a letter, retrieves the corresponding records for that letter and displays them. This is tantamount to displaying a single section of the single-page index:

 sub display_subindex  { my ($dbh, $letter) = @_;  my ($sth, @nav_bar, @item_list);      @nav_bar = gen_nav_bar ($dbh, $letter);      if (!@nav_bar)      {         print p ("The directory has no entries.");          return;      }      print p ("Staff directory entries for the letter '$letter'");      print hr (), center (@nav_bar), hr ();      # Retrieve and display the list of items for a specific letter      $sth = $dbh->prepare (                     "SELECT id, CONCAT(lname,', ',fname) AS name                      FROM staff WHERE lname LIKE ? ORDER BY name");      $sth->execute ($letter . "%");      while (my ($id, $name) = $sth->fetchrow_array ())      {         # Save current item in list          push (@item_list,                   a ({-href => url () . "?id=$id"}, escapeHTML ($name)));      }      $sth->finish ();      # Print list      print ul (li (\@item_list)) if @item_list;      # Display navigation bar again      print hr (), center (@nav_bar), hr ();  } 

The version of gen_nav_bar() in staffm.pl is a little more complex than for the single-page index. The URLs reinvoke staffm.pl to display a particular page of the directory. It also takes some care to generate static text rather than a link, if the link would point to the current page (that s why display_subindex() passes a letter to it as an argument):[3]

[3] display_index() also passes a second argument, but the value is the empty string. This matches none of the link letters, so all the links are active exactly what we want for the main index page.

 sub gen_nav_bar  { my ($dbh, $letter) = @_;  my $letter_ref;      # Get reference to array of distinct letters represented by staff entries      $letter_ref = $dbh->selectcol_arrayref (                         "SELECT DISTINCT UPPER(LEFT(lname,1)) AS letter                          FROM staff ORDER BY letter");      return () if !$letter_ref || !@{$letter_ref};   # error or empty result?      # Turn each letter into a relative link to corresponding section      # section of index page. If the link would point to the current page      # (indicated by $letter), generate static text instead.      return (map { "["              . ($letter eq $_ ? $_ : a ({-href => url () . "?letter=$_"}, $_))              . "]\n" }} @{$letter_ref});  } 

Suggested Modifications

Modify staffs.pl or staffm.pl to provide multiple indexing styles. For example, the application could provide the option of displaying entries grouped not just by name but by position. (Don t forget to index position to make lookups on that column more efficient.)

The two-tier index approach employed by staffm.pl can be extended to more levels. If your organization is quite large, for example, you could add another level that organizes the directory into departments and contains links that take you to named-based indexes for the staff members in each department. What you have to balance here is the size of the lowest-level index pages versus the number of levels. You can add levels to achieve a finer subdivision of records at the lowest level, but then it takes longer to navigate down through the index. One way to help users when you add levels is to combine link-based navigation with a search form. You can use the techniques described earlier in this chapter to provide a form for finding staff members by name or position, for example.

The material in this chapter has covered search capabilities provided by means of search forms and by means of link-based navigation techniques. The examples shown here should give you lots of ideas for writing applications that provide search access to your own tables. However, I should point out before you dive in and start coding that you may want to have a look at Chapter 8. In that chapter, we ll cover the topic of preserving application information from one invocation of a script to another, including techniques for doing so without sending it all over the network for each request. If you re implementing search applications that manage multiple-page result set displays, you can use sessions as an alternative method for storing information associated with navigation links, rather than passing a lot of information in the URLs for those links.

only for RuBoard - do not distribute or recompile


MySQL and Perl for the Web
MySQL and Perl for the Web
ISBN: 0735710546
EAN: 2147483647
Year: 2005
Pages: 77
Authors: Paul DuBois

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net