Path

ez components / resources / articles and publications / the tree component and yahoo! user interface integration


The Tree component and Yahoo! User Interface integration

Author: Derick Rethans
Date: 2007-12-20 10:39

With the 2007.2 release of eZ Components comes a new component: Tree. This component enables you to create, manipulate and query tree structures in many ways. In addition to its basic functionality, the component also enables you to render the tree structure in different ways, with different visualizers. In this article, we will render output from an XML tree definition in order to create a nice menu using the Yahoo! User Interface library.

The ezcTreeVisitorPlainText class creates a textual representation of the tree structure, which can display the structure on a console. The ezcTreeVisitorGraphViz class creates GraphViz compatible output to generate an image representing your tree structure. Because a tree is often a good representation of the structure of a website, it can be useful to use this class to generate a menu for your website. One of the nicer widgets to do so is Yahoo! User Interface's (YUI's) menu widget. The Tree component also contains a YUI menu visualizer to help you easily create website menus. The menu you see on http://ezcomponents.org/ is based on the Tree component, combined with the YUI menu widget.

The examples in this article use eZ Components as installed with the PEAR installer. See http://ezcomponents.org/docs/install for an installation guide.

YUI Introduction

The Yahoo! User Interface library contains many widgets and many different types of menus. The Tree component's YUI visualizer (ezcTreeVisitorYUI) currently produces XHTML for the "Website Top Nav With Submenus Built From Markup" menu only. An example of this type of menu can be found on the YUI menu widget documentation site. The YUI widgets depend on a specific set of JavaScript files, which you can either copy to your local server or request in your script from the Yahoo! website. YUI also comes with a predefined stylesheet that must be used, although it allows for re-styling with CSS. There is a guide on "skinning" available on the YUI website, although it is a bit limited. There are plenty of other things that can be configured for the YUI menu, but that is beyond the scope of this article. See the YUI documentation for more information.

The menu

The menu that we will be creating in this article is the same one as we use on the eZ Components website and consists of two levels.

There are several ways to store a tree structure with the Tree component. You can either use an XML file through the ezcTreeXml class, or one of the three database tree back-ends. As we will not have to manipulate the tree structure in our example application, we will use the ezcTreeXml class.

Defining the structure

Because we are using an XML file for the tree definition, it is important to establish the allowed XML structure. The ezcTreeXml class documentation already describes the format with a Relax-NG schema, but that can be somewhat hard to read. Below you will find a Relax-NG-Compact schema:

default namespace = "http://components.ez.no/Tree"
namespace etd = "http://components.ez.no/Tree/data"

start =
  element tree {
    attribute prefix { xsd:ID }?,
    node?
  }
node =
  element node {
    attribute id { xsd:ID },
    element etd:data { text }?,
    node*
  }

This schema means:

  1. The root node is the "tree" element, as defined by the "start" definition
  2. The "tree" element can have an optional attribute called "prefix"
  3. The "tree" element can have an optional "node" definition
  4. The "node" definition says that the "node" element requires the attribute "id"
  5. The "node" element can have an optional "data" element from the namespace "etd"
  6. The "node" element can have zero to many "node" elements as children

We will leave out the data parts for now and look only at the structure. Let's start with an XML file for the tree that looks like this:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE tree PUBLIC "" "">
<tree xmlns="http://components.ez.no/Tree" xmlns:etd="http://components.ez.no/Tree/data">
    <node id="root">
        <node id="overview">
            <node id="requirements"></node>
            <node id="license"></node>
            <node id="roadmap"></node>
        </node>
        <node id="docs">
            <node id="install"></node>
            <node id="tutorials"></node>
            <node id="api"></node>
        </node>
        <node id="resources">
            <node id="news"></node>
            <node id="articles"></node>
            <node id="presentations"></node>
        </node>
        <node id="search">
        </node>
    </node>
</tree>

We can load the tree structure with the following code:

<?php
require 'ezc/Base/ezc_bootstrap.php';

// load the tree
$tree = new ezcTreeXml( 'tree.xml', new ezcTreeXmlInternalDataStore() );

We can fetch the node data like this:

// show the data in one of the nodes
$docs = $tree->fetchNodeById( 'docs' );
echo $docs->data, "\n";
?>

As we do not have any data yet, the last line will currently throw an exception:

ezcTreeDataStoreMissingDataException: The data store does not have data stored
for the node with ID 'docs'. in
/home/derick/dev/ezcomponents/trunk/Tree/src/stores/xml_internal.php on line 80

Adding data

The default XML-based data store that comes with the Tree component is accessed through the ezcTreeXmlInternalDataStore class. This data store stores the data as elements of the nodes, like this:

<node id="search">
    <etd:data>Search page</etd:data>
</node>

However, this is not sufficient for the eZ Components site, as we also need to know what type of object belongs to the node. For example, the object could be a PHP script or a rendered reStructuredText file. For the eZ Components website, we solve this by using a database store (ezcTreeDbDataStore), as this can store the data in extra columns of the same database table. Although we will not use the type information when generating the menu in our example, it is a good demonstration of how easily the data stores can be extended. Therefore, in this tutorial we will extend the ezcTreeXmlInternalDataStore class to allow for multiple "fields" of data.

Extending the data store

To extend the data store, we simply create a class that inherits the ezcTreeXmlInternalDataStore class. Then we can use the extended class to load the tree:

<?php
require 'ezc/Base/ezc_bootstrap.php';

class ezcaTreeXmlDataStore extends ezcTreeXmlInternalDataStore
{
}

// load the tree
$tree = new ezcTreeXml( 'tree.xml', new ezcaTreeXmlDataStore() );

As you can see from the example above, we created the ezcaTreeXmlDataStore class. Note that the prefix for the class name is "ezca", not "ezc". In order to prevent naming clashes, we strongly recommend that all classes that are not part of the eZ Components code avoid using the "ezc" prefix. In this case, we use "ezca", which stands for "eZ Components Article". When we run the example using the new class, the same exception is thrown when fetching the node data. This is because we haven't reimplemented the methods of the data store yet.

The ezcTreeDataStore interface describes five methods that should be implemented for each data store. Because our data is embedded in XML data stores, we do not have to reimplement the deleteDataForAllNodes() and deleteDataForNodes() methods. They are not necessary because the data is deleted as soon as a node is removed from the tree. There are still three other methods to reimplement that deal with fetching and storing data. The method for storing data is actually only needed when the tree is manipulated by the script - we will not be using this, so we will not reimplement it. Among the two remaining methods is fetchDataForNodes(), which loops over an ezcTreeNodeList list and runs fetchDataForNode() for each of the nodes in the list. We can therefore just reimplement the fetchDataForNode() method.

Instead of trying to parse complex XML structures, we will store both pieces of information (type of page and name) in the same text element, separated by a colon (:). Below is an example of two nodes with the data presented as described:

<node id="resources">
    <etd:data>rst:Resources</etd:data>
</node>
<node id="search">
    <etd:data>php:Search</etd:data>
</node>

The first field of the data will contain the object type - for example, "rst" for a rendered reStructuredText file, and "php" for a PHP script. The second field contains the nice name to show for this node.

The implementation of the fetchDataForNode() method is copied from the original file. The second-to-last line used to read:

$node->data = $dataElem->firstChild->data;

In the reimplementation it is as follows, in order to parse the data in the modified format:

$node->data = split( ':', $dataElem->firstChild->data );

Here is the script again, with the reimplemented method in place. The last line has been changed to dump an array, as the previous usage of "echo" was suitable when we had only one piece of information in a text element:

<?php
require 'ezc/Base/ezc_bootstrap.php';

class ezcaTreeXmlDataStore extends ezcTreeXmlInternalDataStore
{
    /**
     * Retrieves the data for the node $node from the data store and assigns it
     * to the node's 'data' property.
     *
     * @param ezcTreeNode $node
     */
    public function fetchDataForNode( ezcTreeNode $node )
    {
        $id = $node->id;
        $elem = $this->dom->getElementById( "{$node->tree->prefix}{$id}" );
        $dataElem = $elem->getElementsByTagNameNS( 'http://components.ez.no/Tree/data', 'data' )->item( 0 );
        if ( $dataElem === null )
        {
            throw new ezcTreeDataStoreMissingDataException( $node->id );
        }
        $node->data = split( ':', $dataElem->firstChild->data );
        $node->dataFetched = true;
    }
}

// load the tree
$tree = new ezcTreeXml( 'tree.xml', new ezcaTreeXmlDataStore() );

// show the data in one of the nodes
$docs = $tree->fetchNodeById( 'docs' );
var_dump( $docs->data );
?>

Of course, for this to work, we also need to modify the tree.xml file, to include data for the node in question:

...
<node id="docs">
    <etd:data>rst:Documentation</etd:data>
    ...
</node>
...

The output of the script is now:

array(2) {
  [0]=>
  string(3) "rst"
  [1]=>
  string(13) "Documentation"
}

The full example can be found in the downloadable archive (see the left menu) with examples with the filename extend1.php.

The final XML file is pasted below:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE tree PUBLIC "" "">
<tree xmlns="http://components.ez.no/Tree" xmlns:etd="http://components.ez.no/Tree/data">
  <node id="root">
    <etd:data>php:Homepage</etd:data>
    <node id="overview">
      <etd:data>rst:Overview</etd:data>
      <node id="requirements"><etd:data>rst:PHP Requirements</etd:data></node>
      <node id="license"><etd:data>rst:License</etd:data></node>
      <node id="roadmap"><etd:data>rst:Roadmap</etd:data></node>
    </node>
    <node id="docs">
      <etd:data>rst:Documentation</etd:data>
      <node id="install"><etd:data>rst:Installation Guide</etd:data></node>
      <node id="tutorials"><etd:data>php:Tutorials</etd:data></node>
      <node id="api"><etd:data>php:API Reference</etd:data></node>
    </node>
    <node id="resources">
      <etd:data>php:Resources</etd:data>
      <node id="news"><etd:data>php:News Archive</etd:data></node>
      <node id="articles"><etd:data>php:Articles and Publications</etd:data></node>
      <node id="presentations"><etd:data>php:Presentation Slides</etd:data></node>
    </node>
    <node id="search">
      <etd:data>php:Search</etd:data>
    </node>
  </node>
</tree>

Example plain text rendering

There are a few available classes that render the tree in visually appealing ways. They implement the visitor pattern and can be used to go through a tree to generate nice output. The ezcTreeVisitorPlainText class renders a tree for console-based output. We will start with this and then modify it later to generate the YUI menu. Using our working script, we replace the code under the "show the data in one of the nodes" comment (which fetched the nodes and dumped the resulting array) with the following:

// display the tree
$visitor = new ezcTreeVisitorPlainText();
$tree->accept( $visitor );
echo (string) $visitor;
?>

This produces the following output:

root
├─overview
│ ├─requirements
│ ├─license
│ └─roadmap
├─docs
│ ├─install
│ ├─tutorials
│ └─api
├─resources
│ ├─news
│ ├─articles
│ └─presentations
└─search

This example's source code can be found in the archive with examples with the filename visitor1.php.

Unfortunately, it does not use the nice names that we created earlier for the nodes. Instead, it outputs the respective node IDs. In order to solve this, we need to subclass the ezcTreeVisitorPlainText class. We override the visit() method of this class as follows:

class ezcaTreeVisitorPlainText extends ezcTreeVisitorPlainText
{
    public function visit( ezcTreeVisitable $visitable )
    {
        if ( $visitable instanceof ezcTreeNode )
        {
            if ( $this->root === null )
            {
                $this->root = $visitable->data[1];
            }

            $parent = $visitable->fetchParent();
            if ( $parent )
            {
                $this->edges[$parent->data[1]][] = $visitable->data[1];
            }
        }
        return true;
    }
}

The original class had ->id instead of the parts that now have ->data[1]. The new class thus grabs the nice names instead of the node IDs. When we modify the original script to use the new inherited visitor class, ezcaTreeVisitorPlainText, the output turns into the more pleasant display below:

Homepage
├─Overview
│ ├─PHP Requirements
│ ├─License
│ └─Roadmap
├─Documentation
│ ├─Installation Guide
│ ├─Tutorials
│ └─API Reference
├─Resources
│ ├─News Archive
│ ├─Articles and Publications
│ └─Presentation Slides
└─Search

The full example can be found in the archive with examples with the filename extend2.php.

Adding the menu to the site

The YUI visitor class works quite similar to the plain text visitor class. However, first we need to integrate the YUI library into the website.

Adding the YUI parts

First of all, you need to include the correct JavaScript. We refer to them from the Yahoo! servers, but of course you can also download and refer to them locally. The lines below should be placed in the "head" section of your HTML file:

<!-- Dependencies -->
<script type="text/javascript" src="http://yui.yahooapis.com/2.3.1/build/yahoo-dom-event/yahoo-dom-event.js"></script>
<script type="text/javascript" src="http://yui.yahooapis.com/2.3.1/build/container/container_core-min.js"></script>
<!-- Source File -->
<script type="text/javascript" src="http://yui.yahooapis.com/2.3.1/build/menu/menu-min.js"></script>

In the same "head" section you should also refer to the default CSS for the YUI menu:

<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.3.1/build/menu/assets/skins/sam/menu.css"/>

The two snippets above make the YUI accessible, but they will not produce anything until you add some JavaScript code to attach the YUI menu classes and JavaScript to the menu itself.

Rendering the menu

To produce the XHTML in a format that the YUI accepts, first we use the ezcTreeVisitorYUI class instead of the ezcTreeVisitorPlainText class that we used before:

// display the tree
$visitor = new ezcTreeVisitorYUI( 'menu' );
$tree->accept( $visitor );
echo (string) $visitor;
?>

Note the "menu" argument used in ezcTreeVisitorYUI's constructor. This is the XHTML ID that is set on the top-level element used for rendering the menu. You will need to use the same ID when attaching the YUI JavaScript to the menu.

By default, data stores return only a string, but we have modified the data store to return an array. Because of this, the script will now output lots of warnings like the following:

Warning: htmlspecialchars() expects parameter 1 to be string, array given
in /home/derick/dev/ezcomponents/trunk/Tree/src/visitors/yui.php on line 101

In order to work around this, we need to extend the visitor class and override the formatData() method. We do this with the following code:

class ezcaTreeVisitorYUI extends ezcTreeVisitorYUI
{
    protected function formatData( $data, $highlight )
    {
        $data = $data[1];
        $data = htmlspecialchars( $data );
        return $data;
    }
}

// display the tree
$visitor = new ezcaTreeVisitorYUI( 'menu' );
$tree->accept( $visitor );
echo (string) $visitor;
?>

The script now outputs the XHTML content that the YUI JavaScript wants in order to render the menu. You can find this script in the archive with examples with the filename yui2.php.

To apply the YUI library and stylesheets, we need to put one more thing in the "head" section of the webpage. This code attaches the YUI menu to the XHTML tag with the ID that we used before ("menu"):

<script type="text/javascript">
YAHOO.util.Event.onContentReady('menu', function () {
    var oMenu = new YAHOO.widget.MenuBar('menu', { autosubmenudisplay: true, showdelay: 200 });

    oMenu.render();
});
</script>

Tying everything together, we end up with the following code to render the YUI menu. This example can be found in the downloadable archive (see the left menu) with examples with the filename full1.php:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.3.1/build/menu/assets/skins/sam/menu.css"/>

<script type="text/javascript" src="http://yui.yahooapis.com/2.3.1/build/yahoo-dom-event/yahoo-dom-event.js"></script>
<script type="text/javascript" src="http://yui.yahooapis.com/2.3.1/build/container/container_core-min.js"></script>
<script type="text/javascript" src="http://yui.yahooapis.com/2.3.1/build/menu/menu-min.js"></script>

<script type="text/javascript">
YAHOO.util.Event.onContentReady('overview', function () {
    var oMenu = new YAHOO.widget.MenuBar("menu", { autosubmenudisplay: true, showdelay: 200 });

    oMenu.render();
});
</script>
</head>
<body  class="yui-skin-sam">
<?php
require 'ezc/Base/ezc_bootstrap.php';

class ezcaTreeXmlDataStore extends ezcTreeXmlInternalDataStore
{
    public function fetchDataForNode( ezcTreeNode $node )
    {
        $id = $node->id;
        $elem = $this->dom->getElementById( "{$node->tree->prefix}{$id}" );
        $dataElem = $elem->getElementsByTagNameNS( 'http://components.ez.no/Tree/data', 'data' )->item( 0 );
        if ( $dataElem === null )
        {
            throw new ezcTreeDataStoreMissingDataException( $node->id );
        }
        $node->data = split( ':', $dataElem->firstChild->data );
        $node->dataFetched = true;
    }
}

// load the tree
$tree = new ezcTreeXml( 'tree.xml', new ezcaTreeXmlDataStore() );

class ezcaTreeVisitorYUI extends ezcTreeVisitorYUI
{
    protected function formatData( $data, $highlight )
    {
        $data = $data[1];
        $data = htmlspecialchars( $data );
        return $data;
    }
}

// display the tree
$visitor = new ezcaTreeVisitorYUI( 'menu' );
$tree->accept( $visitor );
echo (string) $visitor;
?>
</body>
</html>

When rendered in the browser, the menu looks like this:

../../images/articles/2007-12-20-menu1.png

Styling

Styling the menu is done with CSS. The YUI widget's default CSS elements all use the yui-skin-sam namespace. Overriding specific classes of YUI widgets is therefore quite easy. However, there are a lot of different elements that can be re-styled - some advanced CSS skills might be required to make the menu look exactly as you would like. Firebug is an excellent browser plugin that helps you to inspect and edit the CSS and HTML on your site.

As an example, we're going to style the menu bar in the eZ Components color: indigo. The first step is to tweak the border around the menu, and the menu background. We do this by overriding the following CSS:

.yui-skin-sam .yuimenubar {
    border: 1px solid #55517b;
    background: #55517b;
}

.yui-skin-sam .yuimenubaritemlabel {
    border-color: #55517b;
}

.yui-skin-sam .yuimenu .bd {
    background: #55517b;
}

As the text is no longer readable, we change the text color to white, and also change the font to a sans-serif font:

.yui-skin-sam .yuimenubaritemlabel,
.yui-skin-sam .yuimenuitemlabel
{
        font-family: sans-serif;
        color: white;
}

The menu now looks like this:

../../images/articles/2007-12-20-menu2.png

Then, we change the background and text color of selected items with the following CSS:

.yui-skin-sam .yuimenubaritem a.selected {
        background: #ffffff;
        color: #55517b;
}

We also need to change the color of the arrow on the right side of each non-selected menu item to white. The YUI uses a special trick for rendering images. Instead of having a small image file for each element, it uses what is called a "sprite" file, which is one image with all of the images for all of the elements laid out on a grid. From this, the needed part for each element is specified in the CSS. Some information about this technique can be found here. We created our own sprite file, which can be found here. The CSS below specifies the correct position within the image for each element's image:

.yui-skin-sam .yuimenubarnav .yuimenubaritemlabel .submenuindicator {
        background-position:5px -21px;
        height:8px;
        left:auto;
        margin-top:-3px;
        right:8px;
        text-indent:8px;
        top:50%;
        width:16px;
}

.yui-skin-sam .yuimenuitemlabel .submenuindicator,
.yui-skin-sam .yuimenuitemlabel .checkedindicator,
.yui-skin-sam .yuimenubaritemlabel .submenuindicator {
        background:transparent url(2007-12-20-sprite.gif) no-repeat scroll 0%;
        overflow:hidden;
        position:absolute;
}

.yui-skin-sam .yuimenubarnav a.selected .submenuindicator {
        background:transparent url(2007-12-20-sprite.gif) repeat-x scroll 5px -5px;
}

This is the end result of all the CSS tweaking:

../../images/articles/2007-12-20-menu3.png

All the CSS overrides from above can be found in the ezc.css file in the downloadable archive. We still need to hook the CSS files into the HTML file, which we do by adding the following line after the existing stylesheet line for the default YUI styles:

<link rel="stylesheet" type="text/css" href="ezc.css"/>

The full example can be found in the downloadable archive (see the left menu) with examples with the filename full2.php.

Conclusion

In this article, we introduced the Tree component and showed you how to generate menus using one of the data store options - an XML file. Starting with the plain text renderer, we generated output suitable for display on a console. Then, we used the Tree component's YUI visualizer to generate XHTML content from the same data. The XHTML content can be processed by the YUI library to produce a website menu. From there, we explored how to customize the style of the menu widget.