Building a simple ColdFusion Token/Template System

As ColdFusion developers, we build a lot of different types of applications. For the most part though these apps come down to simple content management systems. We build out forms and let our clients enter blocks of text that then get put together nicely on the front end. While the actual view of the application is dynamic, typically the text itself is rather static. So for example, this blog post comprises a title and a body, and is certainly dynamic when viewed by the public, but the actual words themselves are just straight text. Sometimes we need to provide a way to add an additional level of dynamicness (yes, I know that isn’t a word, but I’m going for it anyway). A great example of this is an email template. Your web site may send out emails to users on a weekly schedule. The text of the email may need to include customization. It may want to say something like, “Hello #name#”, where name is the person receiving the email. You can do this quite easily in ColdFusion, but what if the client wants more control over the text? What if they want to change “Hello” to “Hiya” or perhaps add a ! after the name? This is where a simple token/template system can come in handy. Here is a quick UDF I built with a few sample uses of it. (And by the way, I’m pretty sure I’ve blogged about this before in the past few years, but it was on my mind this week so I thought I’d whip out another example.)

To begin - let's look at a simple template. You can imagine our client editing this to create a customize email for a product site. Users are allowed to sign up for specific newsletters based on different types of products. Here is the plain text:

Hello {name}, Thank you for signing up for our {newslettertype} news list. Every day we will send you new and exciting emails about {producttype}. Thank you, {source}

I've decided on the use of { and } to wrap the dynamic aspects of the template. Any character could be used really but you want something that stands out from the rest of the text. Now that we've got a block of text, we need to create a ColdFusion UDF that will:

  • Look for and find all the {...} strings in the text
  • Accept data in that contains the "real" values for those tokens
  • Replace the tokens with real data and return the string </ul>

    Here is the UDF I came up with. It is a bit complex so I'll go into the design decisions after the paste.

    <cffunction name="tokenReplace" output="false" returnType="array"> <cfargument name="string" type="string" required="true" hint="String with tokens inside, specified by wrapping with {}"> <cfargument name="data" type="any" required="true" hint="Data used to replace tokens. Can be an array of structs, a struct, or a query object."> <cfset var result = []> <!--- first, find tokens in the string so we know what we can recognize ---> <!--- note that we will use a list, which means we wouldn't support a token with a comma in it, which is fair I think ---> <cfset var knownTokens = ""> <cfset var matches = reMatch("{.+?}", arguments.string)> <cfset var match = ""> <cfloop index="match" array="#matches#"> <cfset knownTokens = listAppend(knownTokens, replaceList(match, "{,}",""))> </cfloop> <!--- based on our data, do different things ---> <cfif isStruct(arguments.data)> <cfset var thisResult = arguments.string> <cfloop index="token" list="#knownTokens#"> <cfset thisResult = rereplace(thisResult, "{" & token & "}", arguments.data[token], "all")> </cfloop> <cfset arrayAppend(result, thisResult)> <cfelseif isArray(arguments.data)> <cfloop index="item" array="#arguments.data#"> <cfset var thisResult = arguments.string> <cfloop index="token" list="#knownTokens#"> <cfset thisResult = rereplace(thisResult, "{" & token & "}", item[token], "all")> </cfloop> <cfset arrayAppend(result, thisResult)> </cfloop> <cfelseif isQuery(arguments.data)> <cfloop query="arguments.data"> <cfset var thisResult = arguments.string> <cfloop index="token" list="#knownTokens#"> <cfset thisResult = rereplace(thisResult, "{" & token & "}", arguments.data[token][currentRow], "all")> </cfloop> <cfset arrayAppend(result, thisResult)> </cfloop> <cfelse> <cfthrow message="tokenReplace: data argument must be either a struct, array of structs, or query"> </cfif> <cfreturn result> </cffunction>

    So the first argument to my UDF, tokenReplace, is the actual string. That should be easy enough to understand. The second argument is a bit more complex. I wanted a system that would allow me to pass different types of data. I imagined a query would be most often used, but I also imagined you may need a "one off" so I also wanted to support structs. I also figured you may have an array of data as well so I added support for arrays of structs. Since I figured you would probably be working with multiple sets of data more often than not, I made the UDF consistently return an array of strings.

    I begin by getting all my tokens. This is done with (edited a bit):

    <cfset matches = reMatch("{.+?}", arguments.string)> <cfloop index="match" array="#matches#"> <cfset knownTokens = listAppend(knownTokens, replaceList(match, "{,}",""))> </cfloop>

    The reMatch finds all the {...} tokens and the loop removes the {} from them. Once I have that, I then split into a branch that handles my three types of data. In all cases though we assume that if you have a token for foo, you have data for foo. I don't care if you send me more data than I need, but I do care if you don't have data for one of my tokens. Right now if you make that mistake, you get an ugly error, but the UDF could be updated to make it more explicit.

    So, let's look at a complete example:

    <!--- Our template ---> <cfsavecontent variable="template"> Hello {name}, Thank you for signing up for our {newslettertype} news list. Every day we will send you new and exciting emails about {producttype}. Thank you, {source} </cfsavecontent> <!--- Our sample data ---> <cfset data = queryNew("id,name,newslettertype,producttype,source","cf_sql_integer,cf_sql_varchar,cf_sql_varchar,cf_sql_varchar,cf_sql_varchar")> <cfset queryAddRow(data)> <cfset querySetCell(data, "id", 1)> <cfset querySetCell(data, "name", "Bob")> <cfset querySetCell(data, "newslettertype", "Weapons of Mass Confusion")> <cfset querySetCell(data, "producttype", "weapons")> <cfset querySetCell(data, "source", "MADD")> <cfset queryAddRow(data)> <cfset querySetCell(data, "id", 2)> <cfset querySetCell(data, "name", "Mary")> <cfset querySetCell(data, "newslettertype", "Death Rays and MegaSharks")> <cfset querySetCell(data, "producttype", "lasers")> <cfset querySetCell(data, "source", "Dr. No")> <cfset queryAddRow(data)> <cfset querySetCell(data, "id", 3)> <cfset querySetCell(data, "name", "Joe")> <cfset querySetCell(data, "newslettertype", "Good Meat to Eat")> <cfset querySetCell(data, "producttype", "food")> <cfset querySetCell(data, "source", "PETA")> <cfset results = tokenReplace(template,data)> <cfdump var="#results#" label="Query Test"> <p/> <cfset s = {name="Luke", newslettertype="Lightsabers", producttype="swords", source="The Empire"}> <cfset results = tokenReplace(template, s)> <cfdump var="#results#" label="Struct Test"> <p/> <cfset s2 = {name="Scott", newslettertype="Beers", producttype="beer", source="The Beer Industry"}> <cfset array = [s,s2]> <cfset results = tokenReplace(template, array)> <cfdump var="#results#" label="Array Test">

    As you can see, I've got my template on top, and then three sets of examples. One query, one struct, and one array of structs. The results show that in all cases, we get nicely customized messages back.

Raymond Camden's Picture

About Raymond Camden

Raymond is a developer advocate. He focuses on JavaScript, serverless and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

Lafayette, LA https://www.raymondcamden.com

Comments