Forum OpenACS Development: A Model for the Navigation Bar

Collapse
Posted by Jim Lynch on

Hi, I recently looked at navigation in openacs, and specifically the navigation bar. This article contains my findings and hopefully some steps you can apply to get a working navigation bar.

A navigation bar is a line which contains a colon-separated list of links followed by a colon then the title (or abbreviated title) of the current page. You should see an example under the OpenACS logo, at the top left of this page.

Beginning to look at the list structures in use for navigation bars, we first see, representing a single element of the navigation bar, a two-element list, one element is the title of the link, the other is the url to go to when that section of the navbar is clicked.

{url title} or [list url title]

The order that certain openacs API calls expect is exemplified by: {URL Title}, and to represent a complete navigation bar, you'd have a list starting with 0 or more of the two-element lists followed by a single element which is the title of the current page.

{ {url title} {url title} ... {url title} title }

or, to build it up programmatically,

[list [list url title] [list url title] ... [list url title] title]

Now, let's look at getting this structure onto the page.

The set of templates in openacs contains code to display a navigation bar. If they notice a variable called "context", this value will be included in the navigation bar. The specific code to set up the context variable, for the case of a templated page, there will be a tcl file with a call to ad_page_contract at the top, and an adp page which contains html. Assuming our page is called foo, there will be foo.tcl and foo.adp files. Here is some minimal code:

foo.tcl:

ad_page_contract {
    demonstrating the context variable 
} -query {
    : (optional, to tell this page what variables to expect) 
    : 
} -properties { 
    context : onevalue 
}

# notice this variable, context, is in the -properties 
# section of the ad_page_contract call; this means context
# needs to be available to the foo.adp file.
set context { {url1 title1} {url2 title2} title3 }

foo.adp:

<master>
<if \@context@ not nil>
<property name="context">@context;noquote@</property>
</if>

Content of page foo )this text will appear on the page below the navigation bar)

SO FAR, this is just the standard ways of doing things, and we're not yet seeing what I came up with.

The next message will introduce the first of my concepts, which is having a variable called context_fragment which holds a list that grows and shrinks as the pages flow.

Collapse
Posted by Dave Bauer on
Hi Jim,

Very interesting. You might want to also note this is called "Breadcrumbs" in the CSS for the pages and by web designers.

There are two basic concepts that are used

1) Show the path to the page you are on, based on the site hierarchy. That's how OpenACS works by default.

2) Show the path the user took, you'd need to be smart here and limit this or it could become unusable. This would be more like the path of breadcrumbs you'd use to find your way out of the forest. This would also be similar to the page / back button / history of your browser.

I'd have to go back and read some web design books to see the rational behind each type of decision and it probably depends on exactly what your site is. I don't see a lot of breadcrumbs used on the web, perhaps due to more single page style sites?

Maybe you have another idea?

Collapse
Posted by Jim Lynch on
Heya Dave, nice to know you still have your attention on openacs. I'm posting this thing as fast as I can, you'll see what my complete idea is in a few posts.

-Jim

Collapse
Posted by Jim Lynch on

In the middle of my (re)discovery of how the whole navigation bar thing works, I began to concentrate on the context list, with the inconsistant single element at the end, and I decided that my main data structure would be a list containing all the two-element lists, but leaving out the single title element at the end.

So while a value suitable for context might look like:

{ {url1 tltle1} {url2 title2} {url3 title3} {This Page's Title} }

a value suitable for context_fragment would be everything except the last element:

{ {url1 tltle1} {url2 title2} {url3 title3} }

Note that context_fragment is the only variable involved in the navigation bar to get passed from page to page. In the next section, we look at exactly how to do that, and how to make the context_fragment grow and shrink.

Collapse
Posted by Jim Lynch on

The moving value: context_fragment

Each page in my scheme (remember, this whole thing is certainly not the only way), receives a value for context_fragment. This value will remain constant for the entire code of the page. It represents the value of context_fragment applicable to the PREVIOUS page.

To get this value, your page must state in the ad_page_contract, that it wants to receive it. I do that like this:

ad_page_contract {
    this is where the docs and purpose for the page are stated
} -query {
    contect_fragment
} -properties {
    :
    :
}

The page will now complain if the variable context_fragment is not passed.

The first thing we will do with it, is copy it into two variables, one called context and the other called new_cntxt_frag, like this:

set context $context_fragment
set new_cntxt_frag $context_fragment

and set the page title:

set title "Title for this Page"

adjust the value of context to have the title as its last list element, making it ready to present to the master template:

lappend context $title

add a new var containing a url to the current page (remember, this url must pass a value for context_fragment):

set self_url \
    [export_vars \
        -base this-page \
        {context_fragment}]

and adjust new_cntxt_frag with a new two-element {url title} list, the value of which will be passed to "newer" pages, represents the context_fragment value for THIS page (not the previous page), and the navigation bar will be seen to have a new link when the page is rendered:

lappend new_cntxt_frag [list $self_url $title]

and, except for the first page, that's it. Each page does basically the same thing, and the navigation bar is just handled.

Next section: the first page

Collapse
Posted by Jim Lynch on

This whole thing works in the context of your openacs package, which usually has a www/ dir and an index.tcl and index.adp inside.

One thing that happens when the navigation bar is rendered on a page, is links for "main site" and for your package are included in the navigation bar already, so you don't have to handle those at all. So, the index page should NOT arrange to receive a value for context_fragment (i.e., will not have a mention in the -query section of the ad_page_contract for the index page), and in fact will set up the context_fragment with an empty list:

set context_fragment [list ]
and this value will remain constant (as on the other pages).

Just like in the other pages, we will also copy this to context and to new_cntxt_frag, and list-append the title for this (the index) page to the variable context, and list-append a two-element list {url title} to new_cntext_frag, just as described above.

Now that we have this infrastructure on all the pages in our package, what now? How did I get from one page to the next? to the previous? That's in the next section.

Collapse
Posted by Jim Lynch on

Once I have all this in place, how do I go from page to page? Remember that ALL of the pages (under this particular scheme) except for the www/index page will receive a value for context_fragment.

One way to get to a different page is by a link. I have gravitated to using export_vars to create links, and the links have their own ways of passing variables, and the knowledge of how it's done is all in export_vars, and I don't really have to worry about it... all I have to do is put the name of a variable in the appropriate place, and export_vars will either add what's needed to pass it, or fail to include it -- if it's found to be unassigned.

For example, say there is a preferences page (www/preferences.tcl, www/preferences.adp), and there is a list of things on the page. For each item in the list, we want to be able to go to a page detailing that thing, and we want to be able to delete it. Lastly, we want to be able to go back from the preferences page, to where we were when we hit the preferences button or link.

As usual, the preferences page received the value of context_fragment that represented the previous page, and we have the appended values in context and new_cntxt_frag, so the infrastructure is all set up.

First, let's take the detail page, which we'll call one-thing(.tcl and .adp). We have the list on the preferences page because we made a database query that got us the thing_id, and a few other things for display purposes.

for each item in the list, we can get the detail page url using this call to export_vars:

set detail_url \
    [export_vars \
        -base one-thing \
        [list \
            thing_id \
            [list context_fragment $new_cntxt_frag]]]
which we can put into an <a> tag using some repeating structure which also writes out the list of things. Here, we want to pass the value of context_fragment that represents the preferences page, because we want a link in the navigation bar that will take us to the preferences page. This all has the effect of adding a link to the navigation bar, so it appears we're going deeper, so the page flow is preferences -> one-thing. And since the one-thing page has the thing_id variable passed to it, we can query the database for the row that holds the data for the thing, and using the one-thing.adp, we can show the data.

Next: Going to a page that takes an action, then redirects back: deleting a thing

Collapse
Posted by Jim Lynch on

In a similar way we made the urls which pointed at the detail page (but in that case, expanded the navigation bar by one link), we can also use export_vars to create each delete url. This time, the value of context_fragment that exists for this page, needs to be preserved as it is. The way we'll accomplish that, is we'll send the value to the delete "page" (which only does the delete, then redirects back (with the context_fragment value) to the preferences page. So, the link will have the current value of context_fragment, and we build the link like this:

set delete_url \
    [export_vars \
        -base delete-thing \
        {thing_id context_fragment}]

Compare this to the export_vars that makes the one-thing url.

The parts of the delete script relevent to this mechanism:

delete-thing.tcl:

ad_page_contract { deletes a thing, then returns to preferences. } -query { thing_id:integer context_fragment } -properties { context_fragment : onevalue }

# here, you would check to see if the user has permission # to delete

# then, tell the database to delete the thing

# we're done, so redirect back to preferences

set return_url \ [export_vars \ -base preferences \ {context_fragment}]

ad_returnredirect $return_url ad_script_abort


A few facts,,, One, ad_page_contract is how we told the script to expect some vars (here, context_fragment and thing_id), and under normal conditions, it would cause delete-thing.adp to be run, which would create a separate page for the delete script. This gets short-circuited by the ad_script_abort call, so the .adp never gets called. We don't even need the delete-thing.adp file.

Next: going from a "deeper" page to the next "shallower" page

Collapse
Posted by Jim Lynch on

So far, we've seen how to create two different interactions, one where, since we go "deeper" away from the index page, the navigation bar grows, and one where, since we're returning to the same page, the navigation bar stays the same length.

This last kind of interaction is where, since we're returning to a "shallower" page, one which is closer by one link to the index page, we will need to pull off one list item from context_fragment, so that the destination page will have a navigation bar one link shorter than the one on the current page. Here's how we would form the url:

set old_cntxt_frag \
    [lrange $context_fragment 0 end-1]

set back_url \
    [export_vars \
        -base the-destination \
        [list [list context_fragment $old_cntxt_frag]]]
and when the link is taken with this url, the navigation bar will be shortened by one link, as expected.

These are the three kinds of interactions: lengthen by one, stay the same and shorten by one. We've covered those three interactions here using links, and showing how to make the urls using export_vars. Another possibility for getting from one page to another, is through the use of forms. If there's interest, I'll cover some of these interactions using them.

If anyone found this useful, or are having problems, let me know.