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 cf.Objective() 2010 (Minneapolis, MN) with:

Randomly Executing 1..N Nested ColdFusion Custom Tags

By Ben Nadel on
Tags: ColdFusion

After my post yesterday on the exciting work flow involved in randomly executing a nested ColdFusion custom tag, Ray Camden had some comments that inspired me to update the way the tag works. Rather than just randomly executing a single tag, I wanted to provide a way for the user to specify how many child tags to randomly execute. By default, it will execute just one; but, you can now specify any number of child tags or simply use the keyword, "all," to get the entire list to output in a random order:

  • <!--- Import the random switch tags. --->
  • <cfimport prefix="random" taglib="./" />
  •  
  •  
  • <!--- Select one of the following cases randomly. --->
  • <random:switch randomize="2">
  •  
  • <random:case>
  • (1) Hey there, how are you?<br />
  • </random:case>
  •  
  • <random:case>
  • (2) It's so nice to see you!<br />
  • </random:case>
  •  
  • <random:case>
  • (3) I'm sorry, I can't recall your name?<br />
  • </random:case>
  •  
  • <random:case>
  • (4) How's your mother doing?<br />
  • </random:case>
  •  
  • </random:switch>

Notice that the Switch.cfm tag now has a Randomize attribute. If you exclude this attribute, it defaults to 1 (one). You can specify a number, as I am doing in the demo, or you can pass in, "all." When we run the code above, we get our two randomly output values:

(4) How's your mother doing?
(3) I'm sorry, I can't recall your name?

Because we are executing more than one child tag, we need to update the way the tag works; rather than iterating once for collection and once for execution, the Switch.cfm custom tag now needs to iterate once for collection and as many times as is needed to execute the child tags. The number of post-collection iterations is not necessarily equal to the number of tags to execute. If two consecutive randomly chosen indexes are "in order", they will be executed in the same iteration. And so, the worst case scenario (completely backwards) is N iterations whereas the best case scenario (in original order) is one post-collection iteration.

The child tag, Case.cfm, has remained mostly unchanged. The only update I had to do was check the ExecutionMode of the tag before checking with the parent tag. This is something I technically should have done in yesterday's post; but, since there was no mutation of information, it didn't matter. Now, we are actually updating an internal collection with each call, so it becomes important to only perform the check in the proper mode:

Case.cfm

  • <!--- Only check execution in start mode. --->
  • <cfif (THISTAG.ExecutionMode EQ "Start")>
  •  
  • <!--- Get the parent tag reference. --->
  • <cfset VARIABLES.SwitchTag = GetBaseTagData( "cf_switch" ) />
  •  
  • <!--- Check to see if child should execute. --->
  • <cfif NOT VARIABLES.SwitchTag.ChildShouldExecute()>
  •  
  • <!---
  • The switch tag said not to execute, so exit out of
  • this tag before any code in the body can execute.
  • --->
  • <cfexit method="exittag" />
  •  
  • </cfif>
  •  
  • </cfif>

The main tag, Switch.cfm, on the other hand did have to be significantly updated. Rather than just using a single target index, since we might execute 1..N children, I have to build up a collection of child indexes and then randomly shuffle them. Then, rather than a single post-collection iteration, I have to keep iterating until I no longer have indexes left in my collection (I delete each index off the top of the collection as the given child tag executes).

Switch.cfm

  • <cffunction
  • name="ChildShouldExecute"
  • access="public"
  • returntype="boolean"
  • output="false"
  • hint="I expect to be called by a child tag and I return a boolean as to whether the given tag should execute.">
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = {} />
  •  
  • <!--- Get the switch tag context. --->
  • <cfset var CONTEXT = GetBaseTagData( "cf_switch" ) />
  •  
  • <!---
  • Check to see which mode we are in. If we are collection,
  • then we only want the count and we don't want any child
  • tags to execute.
  • --->
  • <cfif (CONTEXT.SwitchMode EQ "Collection")>
  •  
  • <!--- Collection mode. --->
  •  
  • <!--- Add the new index to the index collection. --->
  • <cfset ArrayAppend(
  • CONTEXT.ChildIndexCollection,
  • (ArrayLen( CONTEXT.ChildIndexCollection ) + 1)
  • ) />
  •  
  • <!---
  • Return false since we don't want any child tags to
  • execute at this point.
  • --->
  • <cfreturn false />
  •  
  • <cfelse>
  •  
  • <!--- Execution mode. --->
  •  
  • <!---
  • Now that we're in the execution mode, we have to keep
  • track of the number of tags that call this function.
  • We want to return false unless the given tag is at the
  • FRONT of the index collection.
  • --->
  •  
  • <!--- Increment child index. --->
  • <cfset CONTEXT.ChildIndex++ />
  •  
  • <!---
  • Check to see if the current index matches the
  • target index.
  • --->
  • <cfif (
  • ArrayLen( CONTEXT.ChildIndexCollection ) AND
  • (CONTEXT.ChildIndex EQ CONTEXT.ChildIndexCollection[ 1 ])
  • )>
  •  
  • <!--- Delete the first index. --->
  • <cfset ArrayDeleteAt(
  • CONTEXT.ChildIndexCollection,
  • 1
  • ) />
  •  
  • <!--- This is the correct tag. Let it execute. --->
  • <cfreturn true />
  •  
  • <cfelse>
  •  
  • <!--- This is not the target tag. Do not execute. --->
  • <cfreturn false />
  •  
  • </cfif>
  •  
  • </cfif>
  • </cffunction>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!--- Check to see which mode we are executing. --->
  • <cfswitch expression="#THISTAG.ExecutionMode#">
  •  
  • <cfcase value="Start">
  •  
  • <!--- Param tag attributes. --->
  •  
  • <cfparam
  • name="ATTRIBUTES.Randomize"
  • type="string"
  • default="1"
  • />
  •  
  • <!---
  • The randomize attribute determines how the child tags
  • get randomized. There are several possibilities
  • including keywords and numbers:
  •  
  • 1 .. N: Outputs the given number of random children.
  •  
  • All: Outputs all in random order.
  • --->
  •  
  •  
  • <!---
  • Check to make sure that the ranomize attribute has a
  • valid value.
  • --->
  • <cfif NOT (
  • (ATTRIBUTES.Randomize EQ "All") OR
  • (
  • IsNumeric( ATTRIBUTES.Randomize ) AND
  • (Fix( ATTRIBUTES.Randomize ) EQ ATTRIBUTES.Randomize) AND
  • (ATTRIBUTES.Randomize GT 0)
  • ))>
  •  
  • <!--- Bad param value. --->
  • <cfthrow
  • type="InvalidAttributeValue"
  • message="Randomize must be an integer greater than zero or ALL."
  • />
  •  
  • </cfif>
  •  
  •  
  • <!---
  • In the start mode, we don't yet know how many
  • children we have. Therefore, we have to do one pass
  • over the child tags to gether the count before we
  • actually execute any of them.
  • --->
  • <cfset VARIABLES.SwitchMode = "Collection" />
  •  
  • <!---
  • Keep an array that will contain each index of the
  • child tag. At first, this will be bulit up, then it
  • will be randomized and leveraged.
  • --->
  • <cfset VARIABLES.ChildIndexCollection = [] />
  •  
  • <!---
  • Keep an index of the child tag that is running. This
  • will only come into play on the secondary passes when
  • we know which target index(es) we want to execute.
  • --->
  • <cfset VARIABLES.ChildIndex = 0 />
  •  
  • </cfcase>
  •  
  •  
  • <!--- ------------------------------------------------- --->
  •  
  •  
  • <cfcase value="End">
  •  
  • <!---
  • Check to see which mode we are in. If we are
  • collecting information or acting on it. If we are
  • collecting, it means that we have to randomize our
  • index collection. If not, we have to loop back until
  • we have no more indexes to execute.
  • --->
  • <cfif (VARIABLES.SwitchMode EQ "Collection")>
  •  
  • <!--- Randomize the index collection. --->
  • <cfset CreateObject( "java", "java.util.Collections" ).Shuffle(
  • VARIABLES.ChildIndexCollection
  • ) />
  •  
  • <!---
  • Now that we have our child index collection
  • randomized, we have to see how many children we
  • need to execute. If we have all, keep all. If we
  • have a number, loop backwards, deleting indexes,
  • until we have the right count.
  • --->
  • <cfif (ATTRIBUTES.Randomize EQ 1)>
  •  
  • <!---
  • Since this it the most common case, optimize
  • for it by just overwriting the entire array.
  • --->
  • <cfset VARIABLES.ChildIndexCollection = [
  • VARIABLES.ChildIndexCollection[ 1 ]
  • ] />
  •  
  • <cfelseif (ATTRIBUTES.Randomize NEQ "All")>
  •  
  • <!---
  • We are selecting 2..(N-1) values, so just
  • start looping backwards deleting them.
  • --->
  • <cfloop condition="(ArrayLen( VARIABLES.ChildIndexCollection ) GT ATTRIBUTES.Randomize)">
  •  
  • <!--- Delete last index. --->
  • <cfset ArrayDeleteAt(
  • VARIABLES.ChildIndexCollection,
  • ArrayLen( VARIABLES.ChildIndexCollection )
  • ) />
  •  
  • </cfloop>
  •  
  • </cfif>
  •  
  • <!---
  • Change the mode to be execution rather than
  • collection. This will signal to the child tags
  • that its time to execute.
  • --->
  • <cfset VARIABLES.SwitchMode = "Execution" />
  •  
  • <!---
  • Loop back to body to allow one of the child tags
  • a chance to execute.
  • --->
  • <cfexit method="loop" />
  •  
  • <cfelse>
  •  
  • <!---
  • We just finished a mode of execution. Let's see if
  • we have any more child tag indexes to execute. If
  • so, loop back.
  • --->
  • <cfif ArrayLen( VARIABLES.ChildIndexCollection )>
  •  
  • <!--- Reset the child index. --->
  • <cfset VARIABLES.ChildIndex = 0 />
  •  
  • <!--- Run through tags again to execute next. --->
  • <cfexit method="loop" />
  •  
  • </cfif>
  •  
  • </cfif>
  •  
  • </cfcase>
  •  
  • </cfswitch>

Because one custom tag execution is the default, and the most likely case, I optimize for that after the index collection has been shuffled. But, if the user wants to execute 2 or more children, I have to loop backwards over the array, deleting indexes that are not relevant. Then, I simply keep looping until no more indexes are left in the collection.

I am not sure how useful a tag like this is, at least not with the more-than-one option. I think the real usefulness of this kind of exploration is just in seeing what kind of work flows can exist within ColdFusion custom tags. I think that custom tags are something that are under utilized, so hopefully seeing this kind of stuff might spark some excitement and some good conversation.




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.