Hierarchical Listings in Views 2: Replicating the LinksDB Directory

The LinksDB module provides a nice "it just works" way for implementing a classic Links page. The standout feature is its hierarchical display of the URLs. Even after Views and CCK arrived, the hierarchical display was worth staying with the module. Sadly, with a site to upgrade and no Drupal 6 version of LinksDB in sight, it was time to convert.

This post is part 2 of 2 of how I migrated the CIPS Vancouver Security SIG Links Directory page from LinksDB to CCK/Views. In part 1, Migrating LinksDB Module Data to CCK, I covered migrating LinksDB data into Drupal nodes and taxonomy. In this post I cover creating the URL Links directory page in Views.

Even if you're not interested in LinksDB, this post provides an example of theming Views to display a hierarchical list using taxonomy to define the hierarchy. This same technique was used to render my Drupal Notes page.

Here are before and after screen shots of the SecSIG Links Directory page:

LinksDB Module Page

LinksDB Module Page Screen Shot

Views Replacement of LinksDB Module Page

Views Replacement of LinksDB Module Page Screen Shot

View Definition

The View definition is un-extraordinary. Here is a snapshot of its edit page, an export of it is included at end of this article.

Views Edit Panel Screen Shot

As you would expect, the View is set to filter on Node Type = Directory Link, and the Links Directory vocabulary. The only required field is Taxonomy: Term. The other fields are those to be displayed. Although the hierarchical theming will change the grouping, Sort criteria is still important. In this case we're sorting on Taxonomy Term, then Node: Title.

Not shown in the screen shot is the value of the header. For the convenience of the directory administrator, a node create link is displayed on the page using the following PHP:

<?php if (user_access('create directory_link content')): ?>
  <div class="create-directory-link"><a href="/node/add/directory-link">Add Directory Link</a></div>
  <br class="clearboth">
<?php endif; ?>

Be sure to select the "Display even if view has no result" checkbox.

View Theming

Two theme files are used to create the View:

  • Style output overriding views-view-unformatted.tpl.php
  • Row style output overriding views-view-fields.tpl.php

The code for these theme files is shown below, and they are both available as file downloads. If you use these files, you'll need to rename the extensions back to ".tpl.php".

Style Output Theme File

In addition to providing an array of formatted entries named $rows, Views also makes the "raw" node data available in an array named $views->result. The "raw" information array contains the actual Drupal node data. Most importantly in this instance, it contains the term id (tid) associated with each node.

The default Views style output theme file simply loops over the $rows array, printing each entry in order. To create the hierarchical display, the custom style output theme file does the following:

  1. Taxonomy term data in the $views->result array is used to create a list of nodes, $nodes_by_tid, indexed by taxonomy term. $nodes_by_tid replaces $rows.
  2. A lookup table of parent/child taxonomy terms, $subcategories, is also created. With it, a list of child terms can be found for any given parent.
  3. This information ($nodes_by_tid and $subcategories) is passed to a recursive function that uses a pattern commonly seen for printing parent/child trees like file directories. This function:
    1. Looks in $nodes_by_tid for link entries belonging to the target tid and prints them if they exist
    2. Looks in $subcategories for child terms of the target tid, and if they exist calls itself on each child term
    3. Creates appropriate title headings and adds HTML for collapsible field sets.

The LinksDB module uses Drupal's built-in JavaScript collapsible field set class (familiar from the Drupal administration interface). This was carried over into the Views implementation. The JavaScript file include for collapse.js is also in the style output template.

views-view-unformatted--links-directory--page.tpl.php File

<?php
  drupal_add_js
('misc/collapse.js');

 
// Create a node list indexed by taxonomy term id.
  // $rows contains the formatted nodes,
  // $view->result contains the raw field data
 
$nodes_by_tid = array();
  foreach(
$view->result as $index => $node_fields) {
   
$nodes_by_tid[$node_fields->term_data_tid][] = $rows[$index];
  }

 
// Create a category list indexed by parent. i.e. Parent/child term map
 
$subcategories = array();
 
$tree = taxonomy_get_tree($view->result[0]->term_data_vid);
  foreach(
$tree as $taxonomyobject) {
    foreach(
$taxonomyobject->parents as $parent) {
     
$subcategories[$parent][] = $taxonomyobject;
    }
  }
 
  if (
$nodes_by_tid) {
    print
_phptemplate_rendertree($subcategories, $nodes_by_tid);
  }


/**
 * Recursive function to render nested display tree
 */
function _phptemplate_rendertree($subcategories, $nodes_by_tid, $taxonomy_term = NULL) {
  if (
$taxonomy_term) {
   
$tid = $taxonomy_term->tid;
   
$head = '<fieldset class="collapsible collapsed"><legend><a href="#">' . $taxonomy_term->name . '</a></legend>';
   
$tail = '</fieldset>';
  }
  else {
   
$tid = 0;
  }

 
// Items for this term
 
$term_item_output = '';
  if (
$nodes_by_tid[$tid]) {
    foreach (
$nodes_by_tid[$tid] as $item) {
     
$term_item_output .= $item;
    }
  }

 
// Subcategories of this term
 
$subcategory_output = '';
  if (
$subcategories[$tid]) {
    foreach(
$subcategories[$tid] as $term) {
     
$subcategory_output .= _phptemplate_rendertree($subcategories, $nodes_by_tid, $term);
    }
  }
 
  if (
$term_item_output || $subcategory_output) {
   
$output = $head . $term_item_output . $subcategory_output . $tail;
  }
 
  return
$output;
 
}
?>

Row Style Output Theme File

The row style output template file renders the individual URL entries. Even though we've altered the way the row entries are listed, each row entry (URL entry) is still rendered in the standard way through this theme file.

views-view-fields--links-directory--page.tpl.php File

<?php
/**
 * - $view: The view in use.
 * - $fields: an array of $field objects. Each one contains:
 *   - $field->content: The output of the field.
 *   - $field->raw: The raw data for the field, if it exists. This is NOT output safe.
 *   - $field->class: The safe class id to use.
 *   - $field->handler: The Views field handler object controlling this field. Do not use
 *     var_export to dump this object, as it can't handle the recursion.
 *   - $field->inline: Whether or not the field should be inline.
 *   - $field->inline_html: either div or span based on the above flag.
 *   - $field->separator: an optional separator that may appear before a field.
 * - $row: The raw result object from the query, with all data it fetched.
 *
 * @ingroup views_templates
 */
?>

<div class="link-directory-item">
  <label><?php print $fields['title']->content ?></label>
  <div class="link-url"><?php print $fields['field_link_url']->content ?></div>
  <?php if ($fields['body']->content): ?>
    <div class="description"><?php print $fields['body']->content ?></div>
  <?php endif; ?>
  <?php if ($fields['edit_node'] || $fields['delete_node']): ?>
    <div class="content-mgmt-links">
      <?php print $fields['edit_node']->content . ' ' . $fields['delete_node']->content ?>
    </div>
  <?php endif; ?>
</div>

Content Management

The LinksDB module placed all of the management functions on the directory page. There was not a separate content management page. To reproduce this, the Views Edit and Delete node fields are added to the list of fields output for each entry. Views logic is smart enough to display them only if the user has access to use them. As previously mentioned, a conditional link named "Add Directory Link" is placed in the header for node creation.

Comments

You should put your functions in your template.php, not the tpl.php file because the template can't be loaded multiple times if it defines functions. This can cause your site to crash if you put a view that uses this style on the page twice for some reason.

Second, this is complex and interesting enough that it should probably be a style plugin all its own. That way you get settings. And stuff.

I've agonized about whether to put the function in the template.php file or the style output theme file. The first instance (for the "Drupal" page on this site) has the function in template.php. For this instance I put it in the theme file. I've argued myself back and forth.

Since the function replaces the "foreach($rows as $row)" construct and is only called from this theme file it seemed appropriate to place it in the theme file. I didn't worry about a function name collision because this theme file is only called once. If you're telling me a name collision is possible even in this situation, that would, of course, clinch the template.php location!

I'm currently digging into plugins and handlers. Enlightenment isn't coming as easily as I'd hoped.

Thanks for sharing. Something I was looking for a long time. Now time for me to tryout on a nested taxonomy deeper than you have.

Hi! This is EXACTLY what I was looking for and I'm just at the last stage of trying to style the view. I'm not sure where the 2 tpl files should go. I've tried them in the views/theme folder as well as my theme folders and no joy. Can you please tell me what I am missing?

Thank you so much for writing this up.

views-view-unformatted.tpl.php

return empty result?

thanks
----------
www.toitim.net

Exactly what I was looking for. Thank you for sharing

An excellent and informative blog posting on adding a hierarchical views listing grouped by terms.

I was curious if you had any tips on modifying this code to differentiate the formatting of the term headers (for example - top level terms get h1, sub level terms get h2)

This is something I need to do, but I get the following error:

Cannot redeclare _phptemplate_rendertree() (previously declared in /nfs/c06/h04/mnt/95216/domains/vmwareeventtoolbox.com/html/sites/all/themes/zen/zen/views-view-unformatted--files-hierarchy--page-1.tpl.php:29) in /nfs/c06/h04/mnt/95216/domains/vmwareeventtoolbox.com/html/sites/all/themes/zen/zen/views-view-unformatted--files-hierarchy--page-1.tpl.php on line 61

Figured it out - move the function into template.php to avoid the 'cannot redeclare' error.

Anyone happen to have customized the table style? I'd love to do something like this with tables but I'm new to drupal and no php wiz, so kind of struggling through it on my own.

Great post Dale.
I'd also love to see this grow into a style plugin.

I wanted to chime in regarding function name collisions.
I'm guilty of including functions inside views tpl.php files as well. It's convenient when I want to keep the code consolidated. To get around the function name collision issue; which produces an error like the one Dylan posted below:
Cannot redeclare _phptemplate_rendertree()(previously declared in...
I wrap the function in a function_exist call.
<?php
//if the function does not exist, declare it
if (!function_exists('_phptemplate_rendertree')) {
function _phptemplate_rendertree(){
...
}
}

That does the trick for me.