Any sample Flex code for displaying taxonomy terms in tree control?

Events happening in the community are now at Drupal community events on www.drupal.org.
waldmanm's picture

Hi,

I'm looking to display a multi-level taxonomy tree in a Flex app using the tree control, as a first step for more advanced functionality. Being new to Flex and to services/amfphp, I was wondering if anybody has some sample flex code that uses the taxonomy.getTree service and translates the returned array into something that can be displayed in Flex. I say "translate", because looking at the returned array it doesn't seem to be what Flex would be expecting (though I might be wrong here).

Thanks in advance for any pointers.

Micah

Comments

2 Solutions

carlosg2's picture

1 Drupal side.
2 Flex side.

Both solutions parse each term and adds to his parent as a child.
I test both and find more straight forward the drupal side solution.

Example service:

function taxonomy_service_get_tree($vid, $parent = 0, $max_depth = NULL) {
  $tree = taxonomy_get_tree($vid, $parent, -1, $max_depth);
  return parse_treedata($tree);
}

function parse_treedata($tree) {
  $map = array();
    $startdepth = false;
   $depthlist = array();
 
   foreach($tree as $vocindex => &$vocitem) {
      if ($startdepth === false || $startdepth > $vocitem->depth) {
            $startdepth = $vocitem->depth;
      }
  $depthlist[$vocitem->depth][$vocitem->tid] = &$vocitem;
  }
  ksort($depthlist);
$lastdepth = null;

   foreach($depthlist as $depthlevel => &$depths) {
        $currentdepthdata = array();
      
           if ($depthlevel == $startdepth) {
              foreach($depths as $itemindex => &$item) {
                  $mapindex = count($map);
                   $map[$mapindex] = (array)$item;
                    $currentdepthdata[$itemindex] = &$map[$mapindex];
              }
          } else {
               foreach($depths as $itemindex => &$item) {
                  foreach($item->parents as $parentindex) {
                       $mapindex = count($lastdepthdata[$parentindex]["children"]);
                     $lastdepthdata[$parentindex]["children"][$mapindex] = (array)$item;
                      $currentdepthdata[$itemindex] = &$lastdepthdata[$parentindex]["children"][$mapindex];
                    }
              }
          }
          $lastdepthdata = $currentdepthdata;
        }
  return $map;
}

Flex solution

waldmanm's picture

@carlosg2 - thank you for posting a solution. I've been working on this since I posted my question, and developed a Flex-side solution (see below). I'm curious - why did you feel that it's better to do the translation on the Drupal side?

The following contains some classes to use the services.getTree method and display the taxonomy in a Flex Tree control.
Disclaimer: While I started programming in the 80's, I'm new to Flex. This example works for me, but it might not be 100% optimal. I'd be happy to hear any comments or improvement suggestions.

On to the code. First, a small class in TaxonomyTerm.as for taxonomy term info:

package Drupal
{
public class TaxonomyTerm {
        public var tid:int;         // The original Drupal term id
     public var name:String;
        public var description:String;
     public var weight:int;      // determines display order
        public var vid:int;         // Vocabulary id of the taxonomy to which this term belongs
        // Note: term parents/children relationships are kept elsewhere and not within this object

       public function TaxonomyTerm()
     {
          this.tid = 0;
          this.vid = 0;
          this.name = null;
          this.description = null;
           this.weight = 0;
       }

     public function toString():String {
            return ("[" + name + "]:[" + description + "][" + vid.toString()+ ":" + tid.toString() + ":" + weight.toString() + "]");
       }
  }
}

Two more classes in DrEvent.as and DrFaultEvent.as to define success and fail events:

package Drupal
{
import flash.events.Event;

    public class DrEvent extends Event
{
      public static const RESULT:String = "DrEventResult";
     public var result:Object;

     public function DrEvent(type:String, result:Object, bubbles:Boolean=false, cancelable:Boolean=false)
       {
          super(type, bubbles, cancelable);
          this.result = result;
      }
     
       public override function clone():Event
     {
          return new DrEvent(type,result,bubbles,cancelable);
        }
     
   }
}


package Drupal
{
import flash.events.Event;

   import mx.rpc.events.FaultEvent;
   
  public class DrFaultEvent extends Event
    {
      public static const FAULT:String = "DrFaultEvent";
       public var faultEvent:FaultEvent;

     public function DrFaultEvent(type:String, faultEvent:FaultEvent, bubbles:Boolean=false, cancelable:Boolean=false)
      {
          super(type, bubbles, cancelable);
          this.faultEvent = faultEvent;
      }
     
       public override function clone():Event
     {
          return new DrFaultEvent(type,faultEvent,bubbles,cancelable);
       }
     
   }
}

This is the main class for getting the taxonomy tree:

package Drupal
{
    import flash.events.EventDispatcher;
   
    import mx.managers.CursorManager;
    import mx.rpc.events.FaultEvent;
    import mx.rpc.events.ResultEvent;
    import mx.rpc.remoting.RemoteObject;

    [Event(name="DrEventResult", type="Drupal.DrEvent.DrEventResult")]
[Event(name="DrFaultEvent", type="Drupal.DrFaultEvent.DrFaultEvent")]
  public class Taxonomy extends EventDispatcher
  {
      private var drupalTaxonomy:RemoteObject;

      private var taxChildren:Array;
    
       private var taxTerms:Array;

       public var taxTree:Array;               // this will hold the taxonomy tree after getTree() is called.
     public var treeObjectsByTid:Array;      // Holds pointers to tree objects (terms) indexed by tid
       public var taxVid:int = 0;              // the Drupal vocabulary id for this taxonomy
     
       /
     * Class constrcutor - make sure Drupal taxonomy service is initialized
     */
     
      public function Taxonomy ():void {
         /
init remote object for drupal taxonomy service /
           drupalTaxonomy = new RemoteObject();
           drupalTaxonomy.destination = "amfphp";
           // this works in MXML but is not supported in ActionScript for some reason: drupalTaxonomy.showBusyCursor="true"
         drupalTaxonomy.source="taxonomy";
            drupalTaxonomy.addEventListener(FaultEvent.FAULT, taxonomyFaultHandler);
           drupalTaxonomy.getTree.addEventListener(ResultEvent.RESULT, taxonomyGetTreeHandler);
          
           /
create new arrays for processing taxonomy array returned from Drupal /
         taxChildren = new Array();
         taxTerms = new Array();
            taxTree = new Array();
         treeObjectsByTid = new Array();
        }
     
       /

     * Helper function for recursively building a Flex tree from the taxonomy array returned by Drupal
      * @param parent - term ID from which to startbuilding
      /
     
      private function buildTaxTree (parent:int) : Array {
           var childrenTerms:Array = new Array();
         var child:int;          // counter for children of current term
           var childTid:int;       // tid of current child being processed
            var childObj:Object;
          
           if (taxChildren[parent]) {
             /
Assemble children and their children (if no children will return empty array) /
                for (child=0; child<taxChildren[parent].length; child++) {
                  childTid = taxChildren[parent][child];
                    childObj = new Object();            // Create new object that will be added to the tree
                    childObj.label = taxTerms[childTid].name;
                  childObj.data =  taxTerms[childTid];
                   // Add 'children' property only if there are really children. Otherwise, this term is a leaf.
                    if (taxChildren[childTid]) {
                       // This child has children too - add them recursively
                      childObj.children = buildTaxTree (taxChildren[parent][child]);
                 }
                  childrenTerms.push(childObj);
                 treeObjectsByTid[childTid] = childObj;
             }
          }
          return childrenTerms;
      }
     
       /

        * buildParentBackPointers -
        * Helper function to go over built taxonomy tree and add to each node a pointer back to
       * its parent to enable easy tree traversing.
       * @param parents - array with tree nodes. Function will add parent pointers to all these
      *                   nodes and their children, recursively
      /
        
       private function buildParentBackPointers(parent:Object, children:Array):void {
             var i:int;
            
           for (i=0; i<children.length; i++) {
                 children[i].parent = parent;        // Set back-pointer to parent
                 if ( children[i].children ) {
                  // Handle all (grand)children recursively
                  buildParentBackPointers(children[i], children[i].children);
                }
          }
          return;
        }
     
       /

     * getTree handler function - converts taxonomy tree from Drupal array into a Flex tree
     * @param result - result object returned from Drupal Taxonomy getTree service
     /
    
       private function taxonomyGetTreeHandler(event:ResultEvent):void {
          var drupalTax:Array = event.result as Array;
           var i:int;
         var parent:int;
            var tid:int;
           var term:TaxonomyTerm;
        
           /
Go over all terms and build children pointers for all terms in an array indexed by term id /
          
           for (i=0; i<drupalTax.length; i++) {
                parent = drupalTax[i].parents[0];       // parents[0] is 0 if no parent
                tid = drupalTax[i].tid;
               
               // Keep children references
                if (! taxChildren[parent]) {
                   taxChildren[parent] = new Array();
             }
              taxChildren[parent].push(tid);
            
               // Keep term info
             term =  new TaxonomyTerm();
                term.tid = tid;
                term.name = drupalTax[i].name;
             term.description = drupalTax[i].description;
               term.weight = drupalTax[i].weight;
             term.vid = taxVid;
             taxTerms[tid] = term;               // keep term in array indexed by term id, for future reference
         }
         
           /
Now build taxonomy tree - add all root level terms to tree and then add their
              children recursively, then add a pointer in each node to its parent for easy traversing /

           taxTree = buildTaxTree (0);
            buildParentBackPointers(null, taxTree);

           dispatchEvent(new DrEvent(DrEvent.RESULT, taxTree));
           CursorManager.removeBusyCursor();       // Done with getting taxonomy tree

        }
     
       private function taxonomyFaultHandler(event:FaultEvent):void {
             trace( "!!! Error: Taxonomy services call error [" + event + "]");
         dispatchEvent(new DrFaultEvent(DrFaultEvent.FAULT, event));
            CursorManager.removeBusyCursor();       // Done with getting taxonomy tree
     }
     
       /

     * getTree - imports taxonomy tree from Drupal and returns a Flex-ready tree
        * @param drupVid - Drupal vocabulary ID 
      /
    
       public function getTree(drupVid:int):void {
            CursorManager.setBusyCursor();      // getting the taxonomy might take a while
         if ( taxVid ) {
                /
already got a tax tree - clear previous entries so can be refreshed */
              taxChildren = new Array();
             taxTerms = new Array();
                taxTree = new Array();
         }
          taxVid = drupVid;
          drupalTaxonomy.getTree(drupVid);
       }
     
   }   // class
}  // package

Some clarifying notes:

  1. This uses Drupal services with no session ID and no API key, for simplicity of example. You'll need a services-config.xml file to point to your services URL, etc.

  2. Each tree item is an object that has:

    • the taxonomy term name as its label property
    • the taxonomyTerm object (including all term info, such as description, tid, etc.) as its data property
    • any children objects as an array pointed to by the children property
      This is the structure that the Tree control understands and can display properly.
  3. I also added a pointer in each tree item to its parent, for some tree traversing purposes. This is held in the parent property and built by buildParentBackPointers(). If you don't need this, you can remove the call to this function.

  4. getTree() is the main function and the one called to get the actual taxonomy tree.

Finally, you use it like this:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
  backgroundColor="white" layout="vertical"  width="100%" height="100%"
  paddingBottom="5" paddingLeft="5" paddingRight="5" paddingTop="5"
  creationComplete="init();"  >

    <!-- Connecting to Drupal -->
   
   <mx:Script>
    <![CDATA[
      
   import mx.controls.;
      import mx.rpc.events.
;
    import mx.utils.ArrayUtil;
     import mx.collections.;
  
   import Drupal.
;

      /* taxonomy vars /
   
   public const DEMO_TAX_VID:int = 1;        // vid for demo Taxonomy
     public var demoTax:Taxonomy = new Taxonomy();
      [Bindable]
     public var demoTaxAC:ArrayCollection = new ArrayCollection([{label:"Getting demo taxonomy ..."}]);
  
   public function init():void {
        /
initialize demo taxonomy from Drupal */
         demoTax.addEventListener(DrEvent.RESULT, demoTaxHandler);
     demoTax.addEventListener(DrFaultEvent.FAULT, demoTaxFaultHandler);
        demoTax.getTree(DEMO_TAX_VID);
       }

          /*** Drupal Services/Taxonomy functions ***/
     
          public function demoTaxHandler(event:DrEvent):void {
        demoTaxAC = new ArrayCollection(event.result as Array);
          }
     
          public function demoTaxFaultHandler(event:DrFaultEvent):void{
              Alert.show( "!!! Error: Could not get demo taxonomy: " + event.faultEvent.fault.toString());
          }
      ]]>
    </mx:Script>


    <mx:Panel title="Taxonomy Demo"
      width="100%" height="100%" layout="absolute"
        paddingTop="10" paddingBottom="10" paddingLeft="10" paddingRight="10" >
     <mx:Tree id="DemoTree"
            width="100%" height="100%"
            dataProvider="{demoTaxAC}"  />   
    </mx:Panel>

</mx:Application>

Quite straightforward. You use an array collection as the bindable data provider because this is what Flex wants.

Micah

keep it simple.

carlosg2's picture

In my opinion, Flex side is a good solution in some cases, but the server side is always in charge of providing this kind of data handling, plus let me work in the fun and real stuff.

If you test my service, you only need to cast the resultobject as an arraycollection and thats it.
White Magic from the server side!

Keep it simple is always better for the client side,
no memory, no cpu if some on can handle this for free.

Beest regards

Carlos Garza
Twitter: @prismacore

Thanks

waldmanm's picture

Thanks Carlos. I'll give it a try.

Micah

Services

Group organizers

Group categories

Group notifications

This group offers an RSS feed. Or subscribe to these personalized, sitewide feeds: