Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at the New York ColdFusion User Group (May. 2008) with: Clark Valberg

Generated Content And Looping ColdFusion Custom Tags

By Ben Nadel on
Tags: ColdFusion

Last week, I was doing a little grunt work for Peter Bell; occassionally, he throws me some scraps from the genius buffet on which he dines, and last week, the scrap had to do with encapsulating table display logic (which, of course, I gobbled up with excitement)... OK, enough with the food analogies. When you are looping over a query, an array, or any sort of iterating business object within the context of a table structure, there is a good deal of logic that goes into determining when to open cells or close cells, when to end the current row, when and if to start the next row, and how to fill out a row that doesn't have enough data to display.

I have done a good deal of work with ColdFusion custom tags, and Peter wanted to see if I could somehow encapsulate the display logic into a custom tag in such a way that you could write code along these lines:

  • <start_tag>
  • <!-- RANDOM CELL HTML HERE -->
  • </end_tag>

Here, the "random HTML" would be the in-cell content. Notice that there wouldn't be any logic to create tables, rows, or cells - that would all be taken care of by the ColdFusion custom tag.

The proof-of-concept ColdFusion custom tag that I made for Peter worked, but it didn't sit right in my head. It had some display logic in the Start tag and some display logic in the End tag, and that didn't feel right. I disliked the idea of having my display logic in multiple places - but what could I do? With a ColdFusion custom tag, you can't reach into the content itself, you can only control what goes on around it.

That's what I thought at the time, but then after I emailed Pete the code, I remembered my old friend, THISTAG.GeneratedContent. ColdFusion custom tags are special in that they do not write any of the generated content to the response's content buffer until after the entire tag has finished executing. This means that you can reach in and alter the between-tag content that the user writes.

Of course, I have never done this in the context of a LOOPING tag, only with a single Start-End tag. As it turns out, though, it works in exactly the same way; for each iteration of the looping ColdFusion custom tag, THISTAG.GeneratedContent holds the output of the current iteration only. You can then grab this, change it, and output updated content without issue.

To demonstrate both the encapsulation of table displays and the use of THISTAG.GeneratedContent in the context of a looping ColdFusion custom tag, I have put together this little demo. Here, I have created TableLoop.cfm, a ColdFusion custom tag that iterates over the passed in array to display a table layout with a passed-in number of columns:

  • <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  • <html>
  • <head>
  • <title>ColdFusion Looping Custom Tag</title>
  • </head>
  • <body>
  •  
  • <!--- Create an array of URLs. --->
  • <cfset arrURL = ArrayNew( 1 ) />
  • <cfset ArrayAppend( arrURL, "183/409576168_381383e2d2.jpg" ) />
  • <cfset ArrayAppend( arrURL, "98/211172356_32707375cb.jpg" ) />
  • <cfset ArrayAppend( arrURL, "156/348910787_a8de7e073c.jpg" ) />
  • <cfset ArrayAppend( arrURL, "1431/1232083186_03f56a2483.jpg" ) />
  • <cfset ArrayAppend( arrURL, "1042/1235759902_551bd0c429.jpg" ) />
  • <cfset ArrayAppend( arrURL, "1066/1237934396_241eeace70.jpg" ) />
  • <cfset ArrayAppend( arrURL, "1050/1238192083_ca79c15e84.jpg" ) />
  •  
  •  
  • <cfoutput>
  •  
  • <!---
  • Loop over the array, using the TableLoop
  • custom tag so that it outputs into a table that
  • has 3 columns.
  • --->
  • <cf_tableloop
  • index="strURL"
  • array="#arrURL#"
  • columns="3">
  •  
  • <!--- Output the image. --->
  • <img
  • src="http://farm1.static.flickr.com/#strURL#"
  • width="125"
  • />
  •  
  • </cf_tableloop>
  •  
  • </cfoutput>
  •  
  • </body>
  • </html>

Notice that there is no Table HTML anywhere. Like a regular index loop, we just have an Index variable into which the current "value" is stored. We then use that value to output our table cell content but don't busy ourselves with and table display logic. All we have to do is pass in the array and tell it that we want THREE columns of display and this is what we get:


 
 
 

 
Encapsulated Table Layout Using Looping ColdFusion Custom Tags  
 
 
 

Notice that we have three TDs per table row (TR) as specified by the Columns attribute. Also notice that in the third row, even though we don't have enough data to display, the TableLoop.cfm ColdFusion custom tag creates two empty cells to give the Table valid XHTML.

That was super easy to do because the table display logic is encapsulated by the TableLoop.cfm ColdFusion custom tag:

  • <!---
  • Check to see which mode we are running in. The
  • Start mode will take care of all the tag attribute
  • paraming and validation. The End tag will take care
  • of all the display logic and the array iteration.
  • --->
  • <cfswitch expression="#THISTAG.ExecutionMode#">
  •  
  • <cfcase value="start">
  •  
  • <!--- Param tag attributes. --->
  •  
  • <!---
  • This is the array that we are going to be
  • iterating over. Each array item will be stored
  • into the caller scope.
  • --->
  • <cfparam
  • name="ATTRIBUTES.Array"
  • type="array"
  • />
  •  
  • <!---
  • This is the name of the variable in the caller
  • scope into which we will store the array item.
  • --->
  • <cfparam
  • name="ATTRIBUTES.Index"
  • type="string"
  • />
  •  
  • <!---
  • This is the number of columns that we will show
  • in our table display.
  • --->
  • <cfparam
  • name="ATTRIBUTES.Columns"
  • type="numeric"
  • />
  •  
  •  
  • <!---
  • We need to keep an internal index of where we
  • are in the array. Start the index at 1.
  • --->
  • <cfset THISTAG.Index = 1 />
  •  
  • <!---
  • This is the end index. This will allow us to
  • use short hand for the length of the array.
  • --->
  • <cfset THISTAG.EndIndex = ArrayLen( ATTRIBUTES.Array ) />
  •  
  •  
  • <!---
  • Check to see if we can even perform a first loop
  • iteration. If we cannot, then we must just exit out
  • of the tag completely (if there are no items in the
  • passed-in array, we have nothing to loop over).
  • --->
  • <cfif THISTAG.EndIndex>
  •  
  • <!---
  • Since we have an array item, let's store the
  • first item into the CALLER scope.
  • --->
  • <cfset "CALLER.#ATTRIBUTES.Index#" = ATTRIBUTES.Array[
  • THISTAG.Index
  • ] />
  •  
  • <cfelse>
  •  
  • <!---
  • The array doesn't even have a length, so just
  • exit out of the tag. This will kill both the
  • Start and End tags.
  • --->
  • <cfexit method="exittag" />
  •  
  • </cfif>
  •  
  • </cfcase>
  •  
  • <!--- -------------------------------------------- --->
  • <!--- -------------------------------------------- --->
  •  
  • <cfcase value="end">
  •  
  • <!---
  • Now that the tag has executed, grab the
  • generated content of the tag.
  • --->
  • <cfset THISTAG.Content = THISTAG.GeneratedContent />
  •  
  • <!---
  • Reset the generated content so that we can have
  • better control over the display logic (and display
  • our own generated content). Remember, since this is
  • a ColdFusion custom tag, none of the generated
  • content is flushed to the content buffer until the
  • tag has finished executing.
  • --->
  • <cfset THISTAG.GeneratedContent = "" />
  •  
  •  
  • <!---
  • Check to see if we need to show the openning
  • display table elements. We will only need to do this
  • once on the first loop iteration.
  • --->
  • <cfif (THISTAG.Index EQ 1)>
  •  
  • <!-- BEGIN: Table Loop. -->
  • <table border="1">
  • <tr>
  •  
  • </cfif>
  •  
  •  
  • <!---
  • Output the content in the current cell. Here, we
  • are taking the generating content of the tag and
  • outputting it inside of the current cell.
  • --->
  • <td>
  • <cfset WriteOutput( THISTAG.Content ) />
  • </td>
  •  
  •  
  • <!---
  • Now that we have output the current cell, we have
  • to figure out if the tag is going to iterate again
  • and if so, are we at the end of a row.
  • --->
  • <cfif (
  • (NOT (THISTAG.Index MOD ATTRIBUTES.Columns)) AND
  • (THISTAG.Index LT THISTAG.EndIndex)
  • )>
  •  
  • <!--- Close the current row and start the next. --->
  • </tr>
  • <tr>
  •  
  • </cfif>
  •  
  •  
  • <!--- Increment the index counter. --->
  • <cfset THISTAG.Index = (THISTAG.Index + 1) />
  •  
  •  
  • <!--- Check to see if we can loop again. --->
  • <cfif (THISTAG.Index LTE THISTAG.EndIndex)>
  •  
  • <!---
  • We can loop again. Prepare for the next
  • iteration by storing the next array element
  • in the CALLER scope.
  • --->
  • <cfset "CALLER.#ATTRIBUTES.Index#" = ATTRIBUTES.Array[
  • THISTAG.Index
  • ] />
  •  
  • <!---
  • Exit out of the current tag iteration and let
  • the tag loop over the END execution mode again.
  • --->
  • <cfexit method="loop" />
  •  
  • <cfelse>
  •  
  • <!---
  • Get the column that the loop ended on. We will
  • need this to determine if we need to fill out
  • the table at all.
  • --->
  • <cfset THISTAG.EndColumn = (
  • ((THISTAG.EndIndex - 1) MOD ATTRIBUTES.Columns)
  • + 1
  • ) />
  •  
  • <!---
  • We have finished looping. Since this is a
  • table display, we might need to fill in the
  • rest of the cells (if we didn't have enough
  • data to finish generating cells for the current
  • row). Check to see if the column that we ended
  • on is less than the number of columns we are
  • displaying.
  • --->
  • <cfif (THISTAG.EndColumn LT ATTRIBUTES.Columns)>
  •  
  • <!--- Loop to fill up the empty cells. --->
  • <cfloop
  • index="THISTAG.Index"
  • from="#(THISTAG.EndColumn + 1)#"
  • to="#ATTRIBUTES.Columns#"
  • step="1">
  •  
  • <td>
  • <!-- Empty Cell. --><br />
  • </td>
  •  
  • </cfloop>
  •  
  • </cfif>
  •  
  • </cfif>
  •  
  •  
  • <!---
  • ASSERT: If we ever get this far in the tag, then we
  • know that we have iterated over the entire array and
  • we are done looping (otherwise, the tag would have
  • exited for a loop before this point in the code).
  • --->
  •  
  •  
  • <!--- Output the closing table display elements. --->
  • </tr>
  • </table>
  • <!-- END: Table Loop. -->
  •  
  • </cfcase>
  •  
  • </cfswitch>

If you look at the Start execution mode of this tag, you will notice that it has no display logic. It doesn't start the table, row, or cell for the first iteration. The only actions it does is to param the tag attributes and some tag-based variables. It waits until it gets into the End execution mode of the tag to do any display logic. Thanks to our ability to update and completely reset the THISTAG.GeneratedContent value on a per-iteration basis, we can perform display logic after the fact.

Pretty nifty stuff! Of course, to really make this useful, you would also have to provide attributes or nested tags to allow the user to set display styles. But, compared to the display logic, such updates would be quite easy.




Reader Comments

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.