Custom Tags That Process Data
So far, we have concentrated on creating custom tags that display information, such as the <cf_ShowMovieCallout> custom tag. Many of the custom tags you will write are likely to be similar to it in that they will be in charge of wrapping up several display-related concepts (querying the database, including formatting, outputting the information, and so on).However, you can also create custom tags that have different purposes in life: to process or gather information. This type of custom tag generally doesn't generate any HTML to be displayed on the current page. Instead, these tags perform some type of processing, often returning a calculated result to the calling template.
NOTE
You might call these tags nonvisual, or perhaps number crunchers, to set them apart from tags that generate something visual, such as the <cf_ ShowMovieCallout> and <cf_HelloWorldMessage> examples you have already seen.Introducing the CALLER Scope
ColdFusion defines two special variable scopes that come into play only when you're creating custom tags:
- The ATTRIBUTES Scope
You have already learned about this scope, which passes specific information to a custom tag each time it is used. - The CALLER Scope
Gives a custom tag a way to set and use variables in the template in which you're using the tag (the calling template).
Returning Variables to the Calling Template
Let's say you're writing a custom tag called <cf_PickFeaturedMovie>, which will choose a movie from the list of available films. In the calling template, you plan on using the tag like so:
Inside the custom tag template (PickFeaturedMovie.cfm), you could set a variable using a special CALLER prefix, such as this:
<cf_PickFeaturedMovie>
This prefix would make featuredFilmID available in the calling template as a normal variable. For instance, this code snippet would call the custom tag and then output the value of the variable it returns:
<cfset CALLER.featuredFilmID = 5>
Of course, using these snippets, the value of featuredFilmID would always be 5, so the custom tag wouldn't be all that useful. Listing 23.10 shows how to expand the previous snippets into a useful version of the <cf_PickFeaturedMovie> custom tag. The purpose of the code is to select a single film's ID number in such a way that all films are rotated evenly on a per-session basis.
<cf_PickFeaturedMovie>
<cfoutput>
The featured Film ID is:
#featuredFilmID#
</cfoutput>
Listing 23.10. PickFeaturedMovie1.cfmSetting a Variable in the Calling Template
The <cfparam> tag at the top of this custom tag template establishes a single optional attribute for the tag called dataSource, which will default to ows if not provided explicitly. We begin by using a <cflock> tag to ensure that the code here is thread-safe. Since we are reading and writing to a shared scope, it's important that we control how the code is accessed. We use <cfparam> to default the list of movies to an empty string. If the SESSION variable already exists, this line will do nothing. If the movieList variable equals an empty string, it means we either just created the variable, or the list of movies has already been used. If this is the case, we run a query to grab all the film IDs. We then use the valueList() function to copy out the list of IDs from the query. The listFirst() function is used to get the first film ID from the list, and listRest is used to remove the ID and resave it to the SESSION.movieList variable. Because the final <cfset> uses the special CALLER scope, the featured movie that the tag has chosen is available for the calling template to use normally.Listing 23.11 shows how you can put this version of the <cf_PickFeaturedMovie> custom tag to use in actual code.
<!---
Filename: PickFeaturedMovie.cfm
Author: Nate Weiss (NMW)
Purpose: Creates the <CF_PickFeaturedMovie> custom tag
--->
<!--- Tag Attributes --->
<!--- Use "ows" datasource by default --->
<cfparam name="ATTRIBUTES.dataSource" type="string" default="ows">
<cflock scope="session" type="exclusive" timeout="30">
<!--- List of movies to show (list starts out empty) --->
<cfparam name="SESSION.movieList" type="string" default=">
<!--- If this is the first time we're running this, --->
<!--- Or we have run out of movies to rotate through --->
<cfif SESSION.movieList eq ">
<!--- Get all current FilmIDs from the database --->
<cfquery name="getFilmIDs" datasource="#ATTRIBUTES.dataSource#">
SELECT FilmID FROM Films
ORDER BY MovieTitle
</cfquery>
<!--- Turn FilmIDs into a simple comma-separated list --->
<cfset SESSION.movieList = valueList(getFilmIDs.FilmID)>
</cfif>
<!--- Pick the first movie in the list to show right now --->
<cfset thisMovieID = listFirst(SESSION.movieList)>
<!--- Re-save the list, as all movies *except* the first --->
<cfset SESSION.movieList = listRest(SESSION.movieList)>
</cflock>
<!--- Return chosen movie to calling template --->
<cfset CALLER.featuredFilmID = thisMovieID>
Listing 23.11. UsingPickFeaturedMovie1.cfmUsing a Variable Set by a Custom Tag
The REQUEST Scope," later in this chapter.First, the <cf_PickFeaturedMovie> custom tag from Chapter 19 for details.
<!---
Filename: UsingPickFeaturedMovie1.cfm
Author: Nate Weiss (NMW)
Purpose: Shows how <cf_PickFeaturedMovie>
can be used in a ColdFusion page
--->
&l275>
<head><title>Movie Display</title></head>
<body>
<!--- Page Title and Text Message --->
<h2>Movie Display Demonstration</h2>
<p>The appropriate "Featured Movie" can be obtained by
using the <b><cf_PickFeaturedMovie></b> tag.
The featured movie can then be displayed using the
<b><cf_ShowMovieCallout></b> tag.<br>
<!--- Pick rotating Featured Movie to show, via Custom Tag --->
<cf_PickFeaturedMovie>
<!--- Display Film info as "callout", via Custom Tag --->
<cf_ShowMovieCallout
filmID="#featuredFilmID#">
</body>
</html>
NOTE
The more a custom tag relies on variables in the calling template, the less modular it becomes. So, although the CALLER scope gives you read-write access to variables in the calling template, you should use it mainly for setting new variables, rather than accessing the values of existing ones. If you find that you are accessing the values of many existing variables in the calling template, maybe you should just be writing a normal <cfinclude> style template rather than a custom tag, or maybe the values should be passed into the tag explicitly as attributes.Variable Names as Tag Attributes
In the version of the <cf_PickFeaturedMovie> custom tag shown in Listing 23.10, the selected film ID is always returned to the calling template as a variable named featuredFilmID. Allowing a custom tag to accept an additional attribute often helps determine the name of the return variable in which the custom tag will place information.For instance, for the <cf_PickFeaturedMovie> custom tag, you might add an attribute called returnVariable, which determines the calling template to specify the variable in which to place the featured film's ID number.To use the tag, change this line from Listing 23.10:
to this:
<!--- Pick rotating featured movie to show via custom tag --->
<cf_PickFeaturedMovie>
This makes the custom tag less intrusive because it doesn't demand that any particular variable names be set aside for its use. If for whatever reason the developer coding the calling template wants the selected film to be known as myFeaturedFilmID or showThisMovieID, he or she can simply specify that name for the returnVariable attribute. The calling template is always in control.Appendix B, "ColdFusion Tag Reference," for details.
<!--- Pick rotating featured movie to show via custom tag --->
<cf_PickFeaturedMovie
returnVariable="FeaturedFilmID">
Using <cfparam> with type="variableName"
You have already seen the <cfparam> tag used throughout this chapter to make it clear which attributes a custom tag expects and to ensure that the data type of each attribute is correct. When you want the calling template to accept a variable name as one of its attributes, you can set the type of the <cfparam> tag to variableName.Therefore, the next version of the <cf_PickFeaturedMovie> custom tag will include the following lines:
When the <cfparam> tag is encountered, ColdFusion ensures that the actual value of the attribute is a legal variable name. If it's not, ColdFusion displays an error message stating that the variable name is illegal. This makes for a very simple sanity check. It ensures that the tag isn't being provided with something such as returnValue="My Name", which likely would result in a much uglier error message later on because spaces aren't allowed in ColdFusion variable names.
<!--- Variable name to return selected FilmID as --->
<cfparam name="ATTRIBUTES.returnVariable" type="variableName">
NOTE
In ColdFusion, variable names must start with a letter, and the other characters can only be letters, numbers, and underscores. Any string that doesn't conform to these rules won't get past a <cfparam> of type="variableName". What's nice is that if the rules for valid variable names changes in a future version of ColdFusion, you won't have to update your code.Setting a Variable Dynamically
After you've added the <cfparam> tag shown previously to the <cf_PickFeaturedMovie> custom tag template, the template can refer to ATTRIBUTES.returnVariable to get the desired variable name. Now the final <cfset> variable in Listing 23.10 just needs to be changed so that it uses the dynamic variable name instead of the hard-coded variable name of featuredFilmID. Developers sometimes get confused about how exactly to do this.Here's the line as it stands now, from Listing 23.10:
People often try to use syntax similar like the following to somehow indicate that the value of ATTRIBUTES.returnVariable should be used to determine the name of the variable in the CALLER scope:
<!--- Return chosen movie to calling template --->
<cfset CALLER.featuredFilmID = thisMovieID>
Or they might use this:
<!--- Return chosen movie to calling template --->
<cfset CALLER.#ATTRIBUTES.returnVariable# = thisMovieID>
These are not legal because ColdFusion doesn't understand that you want the value of ATTRIBUTES.returnVariable evaluated before <cfset> is actually performed. ColdFusion will just get exasperated, and display an error message.
<!--- Return chosen movie to calling template --->
<cfset #CALLER.##ATTRIBUTES.returnVariable### = thisMovieID>
Using Quoted <cfset> Syntax
ColdFusion provides a somewhat odd-looking solution to this problem. You simply surround the left side of the <cfset> expression, the part before the equals (=) sign, with quotation marks. This forces ColdFusion to first evaluate the variable name as a string before attempting to actually perform the variable setting. The resulting code looks a bit strange, but it works very well and is relatively easy to read.So, this line from Listing 23.10:
can be replaced with this:
<!--- Return chosen movie to calling template --->
<cfset CALLER.featuredFilmID = thisMovieID>
Listing 23.12 shows the completed version of the <cf_PickFeaturedMovie> custom tag. This listing is identical to Listing 23.10, except for the first and last lines, which are the <cfparam> line and the updated <cfset> line shown previously.
<!--- Return chosen movie to calling template --->
<cfset "CALLER.#ATTRIBUTES.returnVariable#" = thisMovieID>
Listing 23.12. PickFeaturedMovie2.cfmRevised Version of <cf_PickFeaturedMovie>
Listing 23.13 shows how to use this new version of the custom tag. This listing is nearly identical to Listing 23.11, except for the addition of the returnVariable attribute. Note how much clearer the cause and effect now are. In Listing 23.11, the featuredFilmID variable seemed to appear out of nowhere. Here, it's very clear where the showThisMovieID variable is coming from.
<!---
Filename: PickFeaturedMovie2.cfm
Author: Nate Weiss (NMW)
Purpose: Creates the <CF_PickFeaturedMovie> custom tag
--->
<!--- Tag Attributes --->
<!--- Variable name to return selected FilmID as --->
<cfparam name="ATTRIBUTES.returnVariable" type="variableName">
<!--- Use "ows" datasource by default --->
<cfparam name="ATTRIBUTES.dataSource" type="string" default="ows">
<cflock scope="session" type="exclusive" timeout="30">
<!--- List of movies to show (list starts out empty) --->
<cfparam name="SESSION.movieList" type="string" default=">
<!--- If this is the first time we're running this, --->
<!--- Or we have run out of movies to rotate through --->
<cfif SESSION.movieList eq ">
<!--- Get all current FilmIDs from the database --->
<cfquery name="getFilmIDs" datasource="#ATTRIBUTES.dataSource#">
SELECT FilmID FROM Films
ORDER BY MovieTitle
</cfquery>
<!--- Turn FilmIDs into a simple comma-separated list --->
<cfset SESSION.movieList = valueList(getFilmIDs.FilmID)>
</cfif>
<!--- Pick the first movie in the list to show right now --->
<cfset thisMovieID = listFirst(SESSION.movieList)>
<!--- Re-save the list, as all movies *except* the first --->
<cfset SESSION.movieList = listRest(SESSION.movieList)>
</cflock>
<!--- Return chosen movie to calling template --->
<cfset "CALLER.#ATTRIBUTES.returnVariable#" = thisMovieID>
Listing 23.13. UsingPickFeaturedMovie2.cfmUsing the returnVariable Attribute
<!---
Filename: UsingPickFeaturedMovie2.cfm
Author: Nate Weiss (NMW)
Purpose: Shows how <cf_PickFeaturedMovie2>
can be used in a ColdFusion page
--->
&l275>
<head><title>Movie Display</title></head>
<body>
<!--- Page Title and Text Message --->
<h2>Movie Display Demonstration</h2>
<p>The appropriate "Featured Movie" can be obtained by
using the <b><cf_PickFeaturedMovie2></b> tag.
The featured movie can then be displayed using the
<b><cf_ShowMovieCallout></b> tag.<br>
<!--- Pick rotating Featured Movie to show, via Custom Tag --->
<cf_PickFeaturedMovie2 returnVariable="showThisMovieID">
<!--- Display Film info as "callout", via Custom Tag --->
<cf_showMovieCallout filmID="#showThisMovieID#">
</body>
</html>
Using the setVariable() Function
Another way to solve this type of problem is with the setVariable() function. This function accepts two parameters. The first is a string specifying the name of a variable; the second is the value you want to store in the specified variable. (The function also returns the new value as its result, which isn't generally helpful in this situation.)So, this line from Listing 23.12:
could be replaced with this:
<!--- Return chosen movie to calling template --->
<cfset "CALLER.#ATTRIBUTES.returnVariable#" = thisMovieID>
Because the result of the function is unnecessary here, this line can be simplified to:
<!--- Return chosen movie to calling template --->
<cfset temp = setVariable("CALLER.#ATTRIBUTES.
returnVariable#", thisMovieID)>
<!--- Return chosen movie to calling template --->
<cfset setVariable("CALLER.#ATTRIBUTES.returnVariable#", thisMovieID)>
Using struct Notation
One more way a custom tag can return information to the calling document is to simply treat the CALLER scope as a structure.So, this line from Listing 23.12:
could be replaced with this:
<!--- Return chosen movie to calling template --->
<cfset "CALLER.#ATTRIBUTES.returnVariable#" = thisMovieID>
Either methodthe quoted <cfset> syntax mentioned previously, the SetVariable() method shown, or struct notationproduces the same results. Use whichever method you prefer.
<!--- Return chosen movie to calling template --->
<cfset CALLER[ATTRIBUTES.returnVariable] = thisMovieID>