ColdFusion Basics : Nesting Custom Tags
I am posting this here to help someone on the House of Fusion CF-Talk mailing list. This demonstrates a really simple nesting custom tag example. In this demo, there are two custom tags: list.cfm and item.cfm. The item.cfm tags go in the list.cfm to create either a horizontal or vertical layout:
<!--- Our parent list tag. --->
<cf_list align="horizontal">
<!--- A sub tag: item.cfm. --->
<cf_item>
Ashley Thomas
</cf_item>
<!--- A sub tag: item.cfm. --->
<cf_item>
Sarah Vivenzio
</cf_item>
</cf_list>
Before we get into the parent tag, let's explore the child tag first. Actually before we do that, let's just cover some nested tag basics:
A tag can generate content if it has a close tag. That content is available in a variable THISTAG.GeneratedContent.
The generated content of a tag is not available until the tag is running in execution mode: End. Think about it, if we are in the start tag, no content could possibly have been generated.
Child tags associate themselves with parent tags.
A parent tag DOES NOT know about its child (nested) tags until it is in its own End mode execution.
Ok, that being said, let's take a look at the child tag:
<!--- Kill extra output. --->
<cfsilent>
<!---
We only want to associate tag info with the parent in the
first run of the sub tag. Otherwise, we might end up
storing the info twice. Also, no need to validate
attributes twice, the first time is sufficient.
--->
<cfif (THISTAG.ExecutionMode EQ "Start")>
<!---
Associate this tag with the parent tag. We are
going to override the default value for data
collection. It is usually AssocAttribs, which is a
horrible name. We are going to store it in the
structure "Items". This will allow the attribute data
of the tag to be stored in parent tag.
In this case, "list.cfm" is our parent tag. We define
this association via the old school name "cf_list".
--->
<cfassociate
basetag="cf_list"
datacollection="Items"
/>
<!--- Param tag attributes. --->
<cfparam
name="ATTRIBUTES.value"
type="string"
default=""
/>
<!---
Param the trim value flag. We are defaulting this
to true. In that case, we will trim the value of
the GENERATED CONTENT (not the Value attribute).
--->
<cfparam
name="ATTRIBUTES.trimvalue"
type="boolean"
default="true"
/>
</cfif>
<!---
Check to see if this tag as a closing tag. If it does,
then we might be sending the value of the generated
content instead of the value attribute.
--->
<cfif THISTAG.HasEndTag>
<!---
This tag might just be self closing (which would
be considered a closing end tag. In that case, we
won't have any length in our generated content
(the content between the opening and closing tags.
Check the length of the generated contet.
--->
<cfif Len( THISTAG.GeneratedContent )>
<!---
Since we have generated content, we are going
to be using that as our list item value.
Check to see if we need to trim this.
--->
<cfif ATTRIBUTES.trimvalue>
<!---
Trim value and save it into the attributes.
We don't need to store into attributes, but
we already have the value, so why not.
--->
<cfset ATTRIBUTES.value = THISTAG.GeneratedContent.Trim() />
<!--- Erase the generated content. --->
<cfset THISTAG.GeneratedContent = "" />
</cfif>
</cfif>
</cfif>
</cfsilent>
I am not going to comment too much here because the code sample itself is very well commented. The one thing I will touch upon is the CFAssociate tag. I am hard coding the parent tag "cf_list". This does NOT need to be hard coded. You can use the GetBaseTagList() method to make it more dynamic:
<cfassociate
basetag="#ListLast( GetBaseTagList() )#"
datacollection="Items"
/>
I would NOT recommend this as you never really know where the parent tag name will show up in the base tag list. I am just noting this so you can let your imagination run wild.
Ok, now let's take a look at the parent tag:
<!--- Kill extra output. --->
<cfsilent>
<!---
Check to see if we are in the start mode of the tag. There
is no need to param any attributes after the start mode.
--->
<cfif (THISTAG.ExecutionMode EQ "Start")>
<!---
Param the align attribute. This till determine if we
show the list one after another in-line, or if the
list should be displayed as block elements.
Possible values:
- horizontal (default)
- vertical
--->
<cfparam
name="ATTRIBUTES.align"
type="string"
default="horizontal"
/>
</cfif>
</cfsilent>
<!---
Since this is a parent tag that is designed to have child
tags, we can't really do anything until the child tags have
been defined. Therefore, we can only really work in the
End mode of execution.
--->
<cfif (THISTAG.ExecutionMode EQ "End")>
<!---
ASSERT:
At this point, we should have all the child tags
associated with this parent tag. As per the child tags,
all the attribute data should be in a structure:
THISTAG.Items.
--->
<!---
Now, we have to check to see how to display the items.
Vertically or horizontally?
--->
<cfif (ATTRIBUTES.align EQ "vertical")>
<!--- Display veritcally. --->
<!---
Loop over the Items array. Remember, this array
contains the attributes of the child tags. Remember
to use CFOutput tags as custom tags are NOT natural
CFOutput blocks.
--->
<cfoutput>
<cfloop
index="intI"
from="1"
to="#ArrayLen( THISTAG.Items )#"
step="1">
<!--- Output value. --->
<p>
Item #intI#: #THISTAG.Items[ intI ].value#
</p>
</cfloop>
</cfoutput>
<cfelse>
<!---
We are going with the default, which is vertical.
You might think the first CFIF clause should be the
default statement since the default is probably the
most often used. By making the default the ELSE
clause, we don't really have to validate the types
passed in. But that's not really here nor there.
--->
<!---
Loop over the Items array. Remember, this array
contains the attributes of the child tags. Remember
to use CFOutput tags as custom tags are NOT natural
CFOutput blocks.
--->
<cfoutput>
<cfloop
index="intI"
from="1"
to="#ArrayLen( THISTAG.Items )#"
step="1">
<!--- Output value. --->
Item #intI#: #THISTAG.Items[ intI ].value#
</cfloop>
</cfoutput>
</cfif>
</cfif>
Again, the code above is fairly well commented, so I will just let you take it in and process it. I know that the code samples here are not great, I will try to wrap this thing up in a ZIP and put it in the ColdFusion code snippets.
Want to use code from this post? Check out the license.
Reader Comments
Well written easy to follow example. Good work!
Dan,
Thanks :) Just trying to help people learn. Yahoo for learning!
Great work, not only on this article!
Thanks dude! Let me know if can help you with anything (demos, reviewing code, whatever).
Nice tutorial.
I have been experimenting with jQuery tabs. In one session, I used functions to wrap the code. I wanted to see if custom tags would be easier to use (for other users). This was very helpful in making the demo.
@Jerry,
Glad to help out :)
@ben
> cfassociate
This tag is stupid.
Why would the parent tag just want to know what attributes were passed to the child?
How about content?
> GetBaseTagList()
> I would NOT recommend this as you never really know where the parent tag name will show up in the base tag list. I am just noting this so you can let your imagination run wild.
You _do_ know where the parent tag name is in the list.
It's sequential, as in an ancestral hierarchy.
A given tag's parent is always the second item in the array. The first item is the start tag for the given tag. That is, if you use start and end tags (why wouldn't you?).
It's actually a little complicated use this information to actually access the parent, however.
........................................
<!--- get ansestor tags --->
<cfset ancestorTagNames=listToArray(getBaseTagList())/>
<cfset ancestorTagNameInstances={}/>
<cfset ancestorTags=[]/>
<cfloop index="index" from="0" to="#(arrayLen(ancestorTagNames) - 1)#">
<cfset name=ancestorTagNames[index + 1]/>
<cfif (not(structKeyExists(ancestorTagNameInstances, name)))>
<cfset ancestorTagNameInstances[name]=[]/>
</cfif>
<cfset arrayAppend(ancestorTagNameInstances[name], (index + 1))/>
<cfset nameInstance=arrayLen(ancestorTagNameInstances[name])/>
<cfset data=getBaseTagData(name, nameInstance)/>
<cfset arrayAppend(ancestorTags, data)/>
</cfloop>
<!--- get parent tag --->
<cfif (arrayLen(ancestorTagNames) gt 1)>
<cfset hasParentTag=true/>
<cfset parentTag=ancestorTags[1 + 1]/>
<cfelse>
<cfset hasParentTag=false/>
</cfif>
<!--- do something with parent tag --->
<cfif (hasParentTag)>
<cfset arrayAppend(parentTag.childTags, variables)/>
</cfif>
........................................
Things I've with this:
- ASP.NET style master pages
- CFC-backed tags
> [index + 1]
Sorry for the weird syntax.
I am still getting used to arrays starting at 1.
I am currently in denial about it.
@Alex,
As far as CFAssociate, sometimes all the child tags need to do is collect attribute data. In that case, the parent tag only needs to get access to the attribute data of the children. This is quite common when you are using custom tags to create a sort of domain specific language for a feature of your website.
As for GetBaseTagList(), yes, they are in nested order; but, if you have multiple parent tags with the same name, then you might access one incorrectly.
Theoretically, you can always figure out *which* parent your are looking for, but it is certainly not *always* the second custom tag. In fact, with many nested situations, it is the last custom tag (of a certain kind) in the list.
@ben
> which parent
I'd say there's only one. There are other levels, grandparents, great grandparents.
But shouldn't you let your parent talk to your grandparent?
I'm thinking error handling, event handling, et al. These things need to bubble up the chain.
How about this scenario.
........................................
<cf_masterpage>
<cf_masterpagepart id="title">Foo</cf_masterpagepart>
<cf_masterpagepart id="main">
<p>Foo boo goo.</p>
</cf_masterpagepart>
<cf_masterpagepart id="right">
Related:
<cf_list>
<cf_item>Foo</cf_item>
<cf_item>Boo</cf_item>
<cf_item>Goo</cf_item>
</cf_list>
</cf_masterpagepart>
</cf_masterpage>
........................................
You have a custom masterpage tag.
You have specific custom masterpagepart tags, which will replace associated parts in a master page template.
You have a custom list tag inside a masterpagepart.
The custom list tag need not ever know it's part of a masterpagepart, or ultimately a masterpage.
You may want to reuse that tag somewhere else.
........................................
<cf_page>
<div id="main">
<h1>Foo</h1>
<p>Foo boo goo.</p>
</div>
<div id="right">
Related:
<cf_list>
<cf_item>Foo</cf_item>
<cf_item>Boo</cf_item>
<cf_item>Goo</cf_item>
</cf_list>
</div>
</cf_page>
........................................
In fact, that's exactly how the built-in CF tags work.
........................................
<cfsavecontent>
<cfhttp>
<cfhttpparam/>
</cfhttp>
</cfsavecontent>
........................................
The http tag needs to know about its children, and the httpparam tags about their parent.
But the http tag doesn't need to know it's parent is savecontent.
In fact, the httpparam tag is all too redundant. How about some polymorphism, Macromedia/Adobe?
........................................
<cfheader/>
........................................
Parent is page (no parent), set response header.
........................................
<cfhttp>
<cfheader/>
</cfhttp>
........................................
Parent is http, set http request header.
I've been working on some polymorphic tags.
........................................
<cf_list type="ordered|unordered">
<cf_item/>
</cf_list>
<cf_sections>
<cf_item/>
</cf_sections>
<cf_dropdown>
<cf_item/>
</cf_dropdown>
........................................
-AH
@Alex,
Yes, when you have very structured markup, there is only one logical parent tag; and, many of the tags don't even need to know about each other. But, you can start to get very abstract with content collection in which nesting can be N levels deep:
<cf_contentitem>
. . . . <cf_contentitem>
. . . . . . . . <cf_contentitem />
. . . . </cf_contentitem>
</cf_contentitem>
Now, of course, things like this *rarely* happen, but then do happen from time to time. And, when it happens, you have to be careful about which parent you need to talk to.
Now, just we are on the same page, at some point, you DO need to know about the tags you are working with. After all, if they are going to communicate, you need to understand who communicates with who. All I was really saying was that using GetBaseTagList() is not really a necessary way to do this. Rather, it makes more sense (and is generally more clear) to use GetBaseTagData() with the name of the parent you are talking to (and the optional index of the parent instance).