Full Version: Nested Categories - Recursion Using Components
Dream.In.Code > Programming Tutorials > ColdFusion Tutorials
skyhawk133
Just a quick little tutorial on how to create display nested categories from a database using recursion and components. This is the simplest way I've found to accomplish recursion in ColdFusion, but feel free to comment if you know a better way.

In this tutorial, we are working with a single table in a SQL database with 3 columns: id, parent, name.

Here is the database structure:
CODE

categories
-----------------------------------
id (int, auto-increment, primary key)
parent (int)
name (nvarchar)


Create the component

Create a new component called categories.cfc, start with this structure:
CODE

<cfcomponent name="Categories">
    <cffunction name="getCats">
    
  <!--- Parent ID --->
  <cfargument name="parent" type="numeric">
  
  
    </cffunction>
</cfcomponent>


As you may already know, recursion simply calls the same function from within itself. The beauty of using components is there is no need to number or name our function or objects because each call creates a new instance of the object.

Setup the query

In the code above, we've created a function called getCats, it accepts 1 argument: parent. We will use this argument to query for child categories.

CODE

<cfcomponent name="Categories">
    <cffunction name="getCats">
    
  <!--- Parent ID --->
  <cfargument name="parent" type="numeric">

  <!--- Query for Children of Parent --->
  <cfquery name="qCats" datasource="YOUR_DATASOURCE">
  SELECT * FROM categories WHERE parent = '#arguments.parent#'
  </cfquery>    
  
    </cffunction>
</cfcomponent>


Create the output

Now that we've setup a query to get all the nested (child) categories, we should setup the output. We'll do this by setting text to a output variable which will be returned. Remember, you should never need to display anything from inside your functions, it should be returned using cfreturn.

CODE

<cfcomponent name="Categories">
    <cffunction name="getCats">
    
  <!--- Parent ID --->
  <cfargument name="parent" type="numeric">

  <!--- Query for Children of Parent --->
  <cfquery name="cats" datasource="YOUR_DATASOURCE">
  SELECT * FROM categories WHERE parent = '#arguments.parent#'
  </cfquery>    

  <!--- Create Output --->
  <cfset output = "<ul>">
  
  <!--- Loop over all items --->
  <cfloop query="cats">
      <cfset output = "#output# <li>#cats.name#</li>">
      
    <!--- Check Children of this item --->
    <cfif cats.recordcount GT 0>
        <cfset output = "#output# #CreateObject('component', 'categories').getCats(cats.id)#">
    </cfif>
    
  </cfloop>
  <cfset output = "#output#</ul>">
  
  <cfreturn output />


    </cffunction>
</cfcomponent>


We did a lot in that step, but it's really pretty simple. We've looped over the query appending text to our output variable and using #CreateObject('component', 'categories').getCats(cats.id)# we call the component and function from within itself to get child categories for the category the query is currently on. After the loop is finished, we return the output variable.

Putting it on a page

Save your categories.cfc and create a new .cfm file. This is where you'll call your recursive function and display your categories in a nicely organized list.

CODE

<cfoutput>
#CreateObject('component', 'categories').getCats(0)#
</cfoutput>


We instantiate the categories component we just made and call the getCats function. When you created your table to query categories from, all of your root (top-level) categories should have a parent of 0, passing 0 to the parent argument starts at the very top of your nesting and begins to recurse down.

You should see something like this:


  • Parent 1
    • Child 1.1
      • Child 1.2
        • Child 1.2.1
          • Child 1.2.1.1
            • Child 1.2.1.1.1
            • Child 1.2.1.2
              • Child 1.2.1.3
                • Child 1.2.1.3.1
          • Parent 2
            sadara
            thanks for this tutorial! after hours of banging my head against a brick wall, i have a working script.
            billybrag
            hello there,
            i tried this and it works ok, but produces lots of empty UL's

            please can you help me to sort them - an eg of this in action is here... www.organiclinker.com/test.cfm

            thanks
            Mike
            skyhawk133
            You may need to add some code in the output to check for an empty value. Not sure why it would be outputting empty UL's. Do you have anything in your database that might cause this?!
            billybrag
            QUOTE(skyhawk133 @ 23 Aug, 2006 - 06:54 AM) *

            You may need to add some code in the output to check for an empty value. Not sure why it would be outputting empty UL's. Do you have anything in your database that might cause this?!


            No - not that i can see - all i have changed from your code is the variable names to match my db?

            it seems to be a problem with the logic?
            monizzle
            I made a few changes to skyhawk133's code so that it produces valid html. (see this url for an explanation of proper list nesting: http://www.w3schools.com/xhtml/xhtml_html.asp)

            In skyhawk's original code the child ul's were not nested within the parent li's. There may be a more elegant way to do what I have done, so please chime in if you've got one.

            skyhawk133: Thanks for the original post!

            CODE


            <!--- Create Output --->
            <!--- Monizzle: The following IF wrapper makes sure an empty <ul> isn't returned if no children are found with the default parent id  --->
              <cfif qCats.recordcount GT 0>
              <cfset output = "<ul>">
              
              <!--- Loop over all items --->
              <cfloop query="cats">
                <!--- Monizzle: Removed closing </li> from output below to properly nest child <ul>s  --->
                  <cfset output = "#output# <li>#cats.name#">
                  
                <!--- Check Children of this item --->
                <cfif cats.recordcount GT 0>
                    <cfset output = "#output# #CreateObject('component', 'categories').getCats(cats.id)#">
                </cfif>
                <!--- Monizzle: Add closing </li> to the output  --->         
                <cfset output = "#output#</li>">
              </cfloop>
              <cfset output = "#output#</ul>">
              
              <cfreturn output />
            <!--- Monizzle: close IF wrapper --->
            </cfif>

            dduck1934
            instead of Creating a new Object everytime, couldnt you change this line..

            <cfset output = "#output# #CreateObject('component', 'categories').getCats(cats.id)#">


            to just <cfset output = "#output# #getCats(cats.id)#">


            This is a "lo-fi" version of our main content. To view the full version with more information, formatting and images, please click here.
            Invision Power Board © 2001-2008 Invision Power Services, Inc.