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 jQuery Conference 2010 (Boston, MA) with:

Building A Table With Nested ColdFusion Custom Tags And A ColdFusion Component

By Ben Nadel on
Tags: ColdFusion

Previously, I tried to build a table structure using just nested ColdFusion custom tags. In that scenario, the base Table tag stored the rows in local structures and the Row tag stored the cells in a local structure. I didn't quite like the way the code was working. For this attempt, I am aiming for the same outcome, however, instead of storing the data directly, the base Table tag will store a ColdFusion component. This CFC will be responsible for maintain an internal data structure that represents the rows and cells of the table.

The idea here is that the ColdFusion custom tags act as data collectors - they merely execute and figure out what data they have. The ColdFusion component, Data.cfc, is responsible for accepting data, knowing which row or cell to apply it to, and maintaining an internal data structure.

The Data.cfc ColdFusion component is quite simple. It keeps an array of arrays and has a method for adding a row, adding a cell, and getting the rows array:

  • <cfcomponent
  • hint="Stores data for custom tags.">
  •  
  • <!---
  • Run pseudo constructor to set up data structures
  • and set default data values. This will run when
  • the CFC is instantiated.
  • --->
  •  
  • <!---
  • Set up an instance structure to hold instances
  • related data.
  • --->
  • <cfset VARIABLES.Instance = StructNew() />
  •  
  • <!---
  • Set up an array to keep track of data rows. Our
  • table is going to be constructed using an array of
  • arrays for rows of cells.
  • --->
  • <cfset VARIABLES.Instance.Rows = ArrayNew( 1 ) />
  •  
  •  
  • <cffunction
  • name="Init"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="Returns an initialized data instance.">
  •  
  • <!--- Return This reference.--->
  • <cfreturn THIS />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="AddCell"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="Adds the current cell value to the current row.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Cell"
  • type="any"
  • required="true"
  • hint="Any object that implements the ToString() interface."
  • />
  •  
  • <!---
  • Check to see if we have a current row. If we
  • do not, then add a row first.
  • --->
  • <cfif NOT ArrayLen( VARIABLES.Instance.Rows )>
  • <cfset THIS.AddRow() />
  • </cfif>
  •  
  •  
  • <!--- Add the Cell value to the current row. --->
  • <cfset ArrayAppend(
  • VARIABLES.Instance.Rows[ ArrayLen( VARIABLES.Instance.Rows ) ],
  • ARGUMENTS.Cell
  • ) />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="AddRow"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="Adds a row to the current table.">
  •  
  • <!---
  • Append a row to the rows array. Since we are going
  • to model our table rows/cells as an array of
  • arrays, we need to append an empty array to our
  • rows array.
  • --->
  • <cfset ArrayAppend(
  • VARIABLES.Instance.Rows,
  • ArrayNew( 1 )
  • ) />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="GetRows"
  • access="public"
  • returntype="array"
  • output="false"
  • hint="Returns a copy of the rows array.">
  •  
  • <cfreturn VARIABLES.Instance.Rows />
  • </cffunction>
  •  
  • </cfcomponent>

Our index page (index.cfm) has not changed at all. It defines all the table, rows, and cell custom tags:

  • <!--- Impor the tag libraries. --->
  • <cfimport taglib="./" prefix="tag" />
  •  
  • <!--- Define table tags. --->
  • <tag:table>
  •  
  • <tag:row>
  • <tag:cell>
  • Name
  • </tag:cell>
  • <tag:cell>
  • Age
  • </tag:cell>
  • <tag:cell>
  • Mad Sexy
  • </tag:cell>
  • </tag:row>
  •  
  • <tag:row>
  • <tag:cell value="Libby" />
  • <tag:cell value="27" />
  • <tag:cell value="Yes" />
  • </tag:row>
  •  
  • <tag:row>
  • <tag:cell>
  • Kit
  • </tag:cell>
  • <tag:cell>
  • 24
  • </tag:cell>
  • <tag:cell>
  • Yes
  • </tag:cell>
  • </tag:row>
  •  
  • <tag:row>
  • <tag:cell value="Megan" />
  • <tag:cell value="32" />
  • <tag:cell value="No" />
  • </tag:row>
  •  
  • </tag:table>

What has changed is our custom tags. Here, the table tag (table.cfm) now instantiates the Data.cfc when it starts to execute:

  • <!---
  • Check to see which execution mode the tag is running in.
  • We won't have access to the child tag data until we are
  • in the End tag mode.
  • --->
  • <cfswitch expression="#THISTAG.ExecutionMode#">
  •  
  • <cfcase value="Start">
  •  
  • <!---
  • Create an instance of the Data ColdFusion
  • component. This component will keep track of our
  • rows and cells. Each child tag will alter this
  • component appropriately.
  • --->
  • <cfset VARIABLES.Data = CreateObject(
  • "component",
  • "Data"
  • ).Init()
  • />
  •  
  • </cfcase>
  •  
  •  
  • <cfcase value="End">
  •  
  • <!---
  • At this point, all of the nested Row tags have
  • populated the Data component. Now, let's Output
  • the table structure based on the nesting of
  • custom Row and Cell tags.
  • --->
  •  
  • <!--- Get the rows structure that has been created. --->
  • <cfset VARIABLES.Rows = VARIABLES.Data.GetRows() />
  •  
  • <cfoutput>
  •  
  • <table cellspacing="1" cellpadding="3" border="1">
  • <!--- Loop over rows. --->
  • <cfloop
  • index="intRow"
  • from="1"
  • to="#ArrayLen( VARIABLES.Rows )#"
  • step="1">
  •  
  • <!---
  • Each row contains an array of cells. Let's
  • get a short-hand reference to that array
  • of cells so that our subsequent code lines
  • are shorter and easier to read.
  • --->
  • <cfset arrCells = VARIABLES.Rows[ intRow ] />
  •  
  • <tr>
  • <!--- Loop over cells in this row. --->
  • <cfloop
  • index="intCell"
  • from="1"
  • to="#ArrayLen( arrCells )#"
  • step="1">
  •  
  • <td>
  • #ToString( arrCells[ intCell ] )#
  • </td>
  •  
  • </cfloop>
  • </tr>
  •  
  • </cfloop>
  • </table>
  •  
  • </cfoutput>
  •  
  • </cfcase>
  •  
  • </cfswitch>

As you can see, it creates the Data component. Then, when the tag is done executing, instead of looping over its own Rows array, it grabs the Rows array out of the data component and renders the table.

The row and cell ColdFusion custom tags have changed slightly, but not all that much. They both still get pointers to the parent tags, but instead of adding themselves to the parent's data array, they are now both invoking methods of the Data ColdFusion component.

Here is the row tag (row.cfm):

  • <!---
  • Check to see which execution mode the tag is running in.
  • We won't have access to the child tag data until we are
  • in the End tag mode.
  • --->
  • <cfswitch expression="#THISTAG.ExecutionMode#">
  •  
  • <cfcase value="Start">
  •  
  • <!--- Get the parent Table tag. --->
  • <cfset THISTAG.Table = GetBaseTagData( "cf_table" ) />
  •  
  • <!---
  • Add a row to the data collector that is stored
  • in the table (base) tag.
  • --->
  • <cfset THISTAG.Table.Data.AddRow() />
  •  
  • </cfcase>
  •  
  • </cfswitch>

Here is the cell tag (cell.cfm):

  • <!---
  • Check to see which execution mode the tag is running in.
  • We won't have access to the child tag data until we are
  • in the End tag mode.
  • --->
  • <cfswitch expression="#THISTAG.ExecutionMode#">
  •  
  • <cfcase value="Start">
  •  
  • <!---
  • The value attribute can be overriden by the tag's
  • generated content. In the End mode, we will check
  • to see if we have any content.
  • --->
  • <cfparam
  • name="ATTRIBUTES.Value"
  • type="string"
  • default=""
  • />
  •  
  •  
  • <!---
  • Get the parent table tag. We don't care about
  • which row we are in since the Data structure we are
  • referencing below will take care of that logic.
  • --->
  • <cfset THISTAG.Table = GetBaseTagData( "cf_table" ) />
  •  
  • </cfcase>
  •  
  •  
  • <cfcase value="End">
  •  
  • <!---
  • Now, we need to add a cell value to the data
  • collector that is being stored in the base Table
  • tag. Check to see if we have any generated content.
  • --->
  • <cfif THISTAG.GeneratedContent.Trim().Length()>
  •  
  • <!---
  • Since the user did provide generated content,
  • then we will use this instead of the Value
  • tag attribute.
  • --->
  • <cfset THISTAG.Table.Data.AddCell(
  • THISTAG.GeneratedContent.Trim()
  • ) />
  •  
  • <!---
  • Now that we have stored the generated content,
  • we can clear it (the rendering of it will be
  • handled by one of the parent tags).
  • --->
  • <cfset THISTAG.GeneratedContent = "" />
  •  
  • <cfelse>
  •  
  • <!---
  • Since the user did not provide any generated
  • content, just use the tag Value attribute as
  • the value of this cell.
  • --->
  • <cfset THISTAG.Table.Data.AddCell(
  • ATTRIBUTES.Value
  • ) />
  •  
  • </cfif>
  •  
  • </cfcase>
  •  
  • </cfswitch>

Running the above, we get the same output as we did in our previous example:


 
 
 

 
 
 
 
 

I like this method better than the previous. There was something very cumbersome about having to maintain a table data structure in the parent tag and in the row tag; it just felt very hacky. Having the table, row, and cell tags all call this centralized Data ColdFusion component just feels like a slightly more elegant solution.




Reader Comments

To learn the ins and outs of working with ColdFusion custom tags. It was an exploration, not a suggestion as to how to make tables.

Reply to this Comment

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.