A reader (nicely) asked me something before I left for Boston, and I never got around to answering. He had an interesting problem. He wanted to list directories and files, in a recursive fashion, using HTML's unordered list display to handle the directories and their children.
Now I thought this was a simple thing - just use the recurse=true option in <cfdirectory>. However - the more I thought about it - the more difficult it seemed. You can sort the <cfdirectory> result - but not in an way you can simply output with HTML.
My first thought was to switch back to a recursive <cfdirectory>, and while that would work, I assumed I'd lose a lot in terms of speed due to all the file operations. So what I came up with was a mix of recursive CFML and the built-in recursive <cfdirectory> tag:
<cfset initialDir = "c:\apache2\htdocs\testingzone\blogcfc_flex2">
<cfdirectory directory="#initialDir#" recurse="yes" name="files" sort="directory asc">
<cfset display(files,initialDir)>
<cffunction name="display" returnType="void" output="true">
<cfargument name="files" type="query" required="true">
<cfargument name="parent" type="string" required="true">
<cfset var justMyKids = "">
<cfquery name="justMyKids" dbtype="query">
select *
from arguments.files
where directory = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.parent#">
</cfquery>
<cfoutput><ul></cfoutput>
<cfoutput query="justMyKids">
<li>#directory##name#</li>
<cfif type is "Dir">
#display(arguments.files, directory & "" & name)#
</cfif>
</cfoutput>
<cfoutput></ul></cfoutput>
</cffunction>
As you can see, I do the initial <cfdirectory> and have it fetch all the files. The UDF simply handles displaying items from the query. I don't normally do output from UDFs, so to be honest, I feel a bit dirty. I'd probably just wrap it up in a cfsavecontent and return that, but this was written in about 5 minutes. Another problem - note I hard code \ as my file delimiter. I could have made this more dynamic by using a Java call:
<cfset separator = createObject("java","java.io.File").separator>
In general, the use of "/" will work just fine in any OS, however, since I was doing a string comparison in my query, I'd probably want to use the same separator CF used.
Archived Comments
This is really ironic...I was thinking about this exact topic on my way home from work today (although with a slightly different output). Thanks a lot! This will definitely help.
More on the subject of recursion and coincidence. I just came across this tonite before popping over here. http://rickosborne.org/blog...
I'm interested to see how you handle db sorting and recursion Ray. My method has always been on the sloppy side. Including the same file over and over again until there are no more parents to be found.
Emmet - well, that is an example of recursion actually. But I know what you mean - recursion can get messy and is a pain to debug at times.
Minor nit-pick: Nested lists should have the sub list between the li tags of the parent, not after.
Simply moving the closing li in your function so it's after the recursive call to the function accomplishes this.
I only point this out because it's a handy function if you use a menuing system like udm4 (www.udm4.com), but it won't work if the nesting isn't proper.
Thanks Doug - I was a bit unsure on that.
Nice, simple, and elegant! Great!
emmet - For database hierarchies, I tend to prefer to let the database do the work. The only downside is that different RDBMS' have different methods of approaching the problem.
Oracle has by far the best method of handling hierarchical data with the "start with...connect by prior" syntax.
Given a table with id, name and parentid columns, you can get a sorted tree result like so:
select name, level
from table
start with parentid is null
connect by prior id = parentid
order siblings by name asc
The level column is a pseudo column that gives the depth of each row in the hierarchy, so the root is level 1, it's children level 2, and so on.
The "order siblings by" sorts the results appropriately within each node (in this case, alphabetically by name).
You can get a similar result in other databases using CTE (Common Table Expressions), but not nearly so easily. Here's an article that compares the two methods:
http://www-128.ibm.com/deve...
In my experience, from a strictly performance standpoint, either method is preferrable to doing the sorting in the application, particularly if the dataset is large.
That being said, if your requirements dictate database portability, Rick Osborne's method in the link you cited seems to be a pretty decent approach, although I haven't tested it out myself.
Im on MSSQL. Oracle is but a pipe dream. I never really had a problem with my method until I noticed how horribly one of our clients apps are now performing. It's an older CF5 app and I dont think it was ever expected to grow to the level it has. It's now taking over 1000ms to generate a tree. Now that were on CFMX7 I need to explore some other options.
For MSSQL, take a look here:
http://msdn2.microsoft.com/...
This covers using CTEs in MSSQL, and combined with the ibm.com link in my previous comment should give you a solid foundation if you want to try this approach.
Though it's not an unordered list, a tree display might be a better solution and easier. I just whipped up an example and added it as a trackback.
Ray... Forgive the title, I couldn't resist :-)
Wiggy
Thanks Doug. Thats alot to wrap my head around.
Don't forget Forta's example:
http://www.forta.com/blog/i...
I have two dropdown menus in a form. The first is populated with directories using cfdirectory (looping thru to find any of type="dir"). The second dropdown needs to be a list of subdirectories that fall under whatever directory the user chooses in the first dropdown menu. Below is the code I've tried but it's not working. Seems like I need something in the onchange event. I've looked at the cf_twoselectsrelated but can't see how to implement it when the query is a set of directories. I don't see exactly how to use the recurse="true" (or "yes"?) either since it looks like all that will do is put the subdirectories into the same dropdown box as their parent directories. Ultimately, I want to allow users to choose at least upto two directory levels for uploading files. For that, I need the parent dir name and subdir name.Any help/ideas is greatly appreciated!
Doug in Fairbanks
<p>Choose top level folder:<br />
<select name="mnuFolder" onchange="">
<cfdirectory directory="D:\myplace\" name="Parent_Folder">
<cfloop query="Parent_Folder">
<cfif #Parent_Folder.Type# eq "Dir">
<cfoutput>
<option value="#Parent_Folder.Name#\">#Parent_Folder.Name#</option>
</cfoutput>
</cfif>
</cfloop>
</select><br />
Choose subfolder (if any):<br />
<select name="mnuSubFolder">
<cfdirectory directory="D:\myplace\#Parent_Folder.Name#\" name="Sub_Folder">
<cfloop query="Sub_Folder">
<cfif #Sub_Folder.Type# eq "Dir">
<cfoutput>
<option value="#Sub_Folder.Name#\">#Sub_Folder.Name#</option>
</cfoutput>
</cfif>
</cfloop>
</select><br />
What you need is dependant selects. You can find many posts on that if you google. It is a bit too much to discuss here.
After three days, I've found an easier way :
<cfparam default="#GetDirectoryFromPath(GetTemplatePath())#/showcase" name="repertoire"/>
<cfdirectory action="list" directory="#repertoire#" name="allDirectories" recurse="true"/>
<cfoutput query="allDirectories" group="name">
<cfif #allDirectories.type# IS "DIR">
<h3>#allDirectories.name#</h3>
<cfelse>
<pre>#allDirectories.name#</pre>
</cfif>
</cfoutput>
Isn't this going to list all directories in one list? What I need is two selects with the first listing top-level directories and the second listing subdirectories of the top-level directory selected in the first select. I searched for "independent selects" but am coming up empty. Sorry if this is a bit much for this site but this is the closest I've come to a solution.
Thanks,
Doug Wilder
Could someone share an example of how to call this function? I'm new to using UDF, and I'm unsure how to input the correct values for the arguments.
Please check the CF Docs on how to use UDFs. In general though you call them like any other function. The arguments for the UDF in this entry are first a query of files, then the path to the initial directory.
ah, a query of files. that's what I needed to know. Thanks.
Using Ray's example as a starting point I have tweaked it to create a recursive function that 'copies' a folder structure, 3 deep and excludes files.
http://www.stinkylittlefrie...