[ZPT] dynamic navigation menus

Mark McEahern marklists@mceahern.com
Tue, 30 Jul 2002 15:14:10 -0500


I recently started looking at Zope again after a long hiatus.
Fortunately, I had a relatively simple and focused goal that seemed
well-suited to Zope and would allow me to explore something new to
Zope since the last time I looked at it: Zope Page Templates (ZPT).

I've pasted a tutorial I'm writing on ZPT below.  Why am I writing
a tutorial when I haven't the first clue about ZPT?  To be explicit
about my ignorance, if nothing else.  Also, hopefully, if this turns
out to be useful, to contribute something to the community.

My current unresolved questions are:

1.  If I want the user to be able to sort Folders within the ZMI in
    an arbitrary way, is there a simpler/easier/less problematic
    approach than adding an integer property called "sort_order"
    to each Folder and then sorting by that in the ZPT macro?

2.  How do I determine the current section and subsection and use
    that to distinguish from selected and unselected tabs (for
    example)?

3.  How do I determine and set the default section and subsection?

These questions will (hopefully) make more sense in the context of
the (unfinished) tutorial below.

I will continue to post refinements as I solve these problems or
work around them or discover additional problems.  Of course, I welcome
any and all feedback--answers/solutions as well as clarifications that
reveal I'm just approaching the problem in the completely wrong way.

Tutorial for creating a dynamic navigation menu with ZPT
--------------------------------------------------------

My goal is to to create a template for a dynamic navigation menu.

I wanted to use Zope folders for each section of the menu.  So a set
of Zope folders like this:

  section1
  section2
  section3

could render a simple menu that might look like this:

  [ section1 | section2 | section3 ]

Of course, I wanted the flexibility of having subsections.  This:

  section1
    subsection1_1
    subsection1_2
    subsection1_3

might look like this:

  [ <section1> | section2 | section3 ]
  [ subsection1_1 | subsection1_2 | subsection1_3 ]

<section1> indicates that section1 has been selected, so its submenu
is displayed.

The particular look I was after was a tabbed navigation menu with two
levels of menus.  Of course, since I was using nested folders and page
templates, it should be relatively easy, once I'm more familiar with
Zope, to create a different look simply by changing the template.

My first step resulted in something surprisingly close to what I was
after, but it also reflected my relative ignorance of Zope.

I downloaded and installed the latest version of Zope (2.5.1) for
Windows.  Rather than comingling my experiment with everything else in
the Root folder, I created a folder called navtest.  Within navtest, I
created the following Folder objects:

  Id          Title
  images      <empty>
  styles      <empty>
  section1    <empty>
  section2    <empty>
  section3    <empty>

As I was adding these sections, I noticed the ZMI (Zope Management
Interface) required using the mouse more than I would have liked since
I had to access the pulldown menu to select the type of object I
wanted to add (Folder).  One idea for improving this would be to have
an additional Add button when you're creating something that adds the
thing you're creating and brings you back to the same page to add
another.  It could also move the mouse into the Id field with
JavaScript rather than requiring you to Tab to it or click on it to
set the focus.

In addition, both add buttons could have a hotkey so that you could
press Alt+A, for instance, to Add, and Alt+M to add this one and add
another.

Looking through the Zope book chapters on ZPT, I realized I needed to
use a macro:

  http://www.zope.org/Documentation/ZopeBook/AdvZPT.stx#2-19

I created two page templates:

  master_html
  index_html

My first stab at the master page template looked like this:

<code>

<html metal:define-macro="page">
<head>
<title tal:content="here/title">The title</title>
</head>
<body>

<!-- Navigation -->
<table>
  <tr>
    <td>
      <span tal:repeat="section container/objectValues">
        <a tal:attributes="href section/getId"
tal:content="section/title"></a>
      </span>
    </td>
  </tr>
  <tr>
    <td>
      <span tal:repeat="subsection here/objectValues">
        <a tal:attributes="href subsection/getId"
tal:content="subsection/title"></a>
      </span>
    </td>
  </tr>
  <tr>
    <td>
      <!-- Content -->
      <table metal:define-macro="content">
        <tr>
          <td tal:content="here/title">
            The content
          </td>
    </tr>
      </table>
    </td>
  </tr>
</table>

<!-- Footer -->
<div metal:define-macro="footer">
  Copyright &copy; 2002, The Company<br>
  All Rights Reserved.
</div>

</body>
</html>

</code>

There are two obvious problems with this initial template: I'm not
using style sheets and I'm using nested tables rather than CSS to
control the look.  Of course, the details of HOW I achieved the look
and feel were less important at this point than understanding Zope, so
I ignored these problems.

This page template defines the slots that pages within the site can
override, if they like--typically, the only one that will be overriden
is the content slot.

The default page (id: index_html) is really simple since all it has to
do is use the page macro from the master template defined above:

<code>

<html metal:use-macro="container/master_html/macros/page">
</html>

</code>

In order to see what I had so far, I opened a browser and navigated
to:

  http://localhost:8080/navtest/

Here's the HTML:

<code>

<html>
<head>
<base href="http://localhost:8080/navtest/" />

<title></title>
</head>
<body>

<!-- Navigation -->
<table>
  <tr>
    <td>
      <span>
        <a href="images"></a>
      </span>
      <span>
        <a href="styles"></a>
      </span>
      <span>
        <a href="master_html"></a>
      </span>
      <span>
        <a href="index_html"></a>
      </span>
      <span>
        <a href="section1"></a>
      </span>
      <span>
        <a href="section2"></a>
      </span>
      <span>
        <a href="section3"></a>
      </span>
    </td>
  </tr>
  <tr>
    <td>
      <span>
        <a href="images"></a>
      </span>
      <span>
        <a href="styles"></a>
      </span>
      <span>
        <a href="master_html"></a>
      </span>
      <span>
        <a href="index_html"></a>
      </span>
      <span>
        <a href="section1"></a>
      </span>
      <span>
        <a href="section2"></a>
      </span>
      <span>
        <a href="section3"></a>
      </span>
    </td>
  </tr>
  <tr>
    <td>
      <!-- Content -->
      <table>
        <tr>
          <td></td>
    </tr>
      </table>
    </td>
  </tr>
</table>

<!-- Footer -->
<div>
  Copyright &copy; 2002, The Company<br>
  All Rights Reserved.
</div>

</body>
</html>

</code>

Once I looked at the HTML, several problems became obvious:

1.  The sections were rendering, but they weren't visible because I
    had not defined titles for them.

    Solution:  Define the Title for each section/subsection.  Another
    alternative would be to use title_or_id instead of title.

2.  I needed to separate the content folders from folders like images
    and styles that I did not intend to reflect in the menu.

    Solution:  Create a content folder and move the section folders
    into it.

3.  I needed to handle the case of a section not having any
    subsections.

    Solution:  I wasn't sure how to handle this yet, so I punted on
    it.

4.  I anticipated having problems with the URIs.  Notice how Zope
    inserts the <base href=""/> tag.  Will I need to suppress that?
    Will my solution to problem 2 (above) create problems with
    relative URIs?

    Solution:  I wasn't sure how to handle this yet, so again I just
    punted on it.

Despite these problems, I felt confident that my basic approach of
using folders and templates would eventually work.

The next step was to solve the problems that I could solve and see
where that got me.

I used the Cut/Paste feature of ZMI to move the section folders from
the navtest folder to a new content folder.

Then I modified the portion of the master template that renders the
main sections:

<code>

      <span tal:repeat="section container/content/objectValues">
        <a tal:attributes="href python:'content/%s' % (section.getId())"
tal:content="section/title_or_id"></a>
      </span>

</code>

Even though I added titles to each section, I decided to use the
title_or_id attribute instead of just plain title in case I forgot (or
didn't want) to add a title to a folder later on.

I couldn't just use:

  <a tal:attributes="href section/getId" ... />

If I did that, the path to the section would be incomplete since it
wouldn't contain the word "content".  So I had to use a Python string
formatting expression to include the literal name of the folder that
contains the section folders.  I discovered via trial and error that I
couldn't use:

  <a tal:attributes="href python:'content/%s' % (section/getId)"
  ... />

I had to replace section/getId with section.getId().  That's probably
obvious, but I'm not sure precisely HOW I'd document this in the ZPT
documentation.

These modifications solved problems 1 and 2 above, but they revealed a
new set of problems:

5.  I needed to define default positioning within the menu as well as
    a method of determining the current position.

If I navigated to the root of the site:

  http://localhost:8080/navtest

the template would render the images and styles folders as if they
were subsections.  This indicated that I needed to have a default
"position" and that I needed to determine the current "position"
somewhat intelligently.  This notion of position would allow me to
distinguish between the current tab and the other tabs both in the
main menu as well as the submenu.

     Solution: Well, I certainly don't have a solution figured out,
     but I thought one possible solution might be to write a Python
     function that takes the here variable as a parameter and returns
     the current section and subsection.  Thinking about this a little
     bit, I'd need to have a way to "select the first item" (e.g., if
     'here' is the root URI, I wouldn't even have one of the content
     folders in the URI, so I'd need to select the first section and
     its first subsection).  But that implied that I needed to figure
     out how to specify order for the folders because the default
     order (alphabetical by name?) certainly wasn't going to be
     sufficient.

I saw the problem with <base href=""/> (problem 4 above) a little more
clearly when I clicked on the link to section 1 and then clicked the
link to subsection1_1.  When I viewed the source, here's what the
<base/> tag looked like:

<code>

<base
href="http://localhost:8080/navtest/content/section1/subsection1_1/"
/>

</code>

If I then subsequently clicked on section1, I could see the beginnings
of a nasty infinitely expanding <base/> tag.

-