Exploring Tag Islands (Tags In CFScript) In Lucee CFML 5.3.1.13
Yesterday, in my post about using CFML tag syntax in CFScript
, Andrew Kretzer told me that Lucee CFML now offers something called "Tag Islands" (seemingly as of Lucee CFML 5.3.1.13). I had never heard of Tag Islands before; and, other than this rather heated post on the Lucee Dev Forum, there's not really any documentation on the topic. As such, I wanted to take a few minutes and do a quick exploration of these Tag Islands so I could see how they work in Lucee CFML 5.3.1.13.
A Tag Island is a block of code, embedded within a CFScript
tag (or a Script-based ColdFusion Component), that supports tag syntax. So, just like we can use CFScript
to create a block of Script-content inside a tag-based page, we can now use the triple back-tick (```) to create a block of Tag-content inside a script-based page:
<cfscript>
echo( "<p>Pre tag-island</p>" );
```
<p>In tag-island</p>
```
echo( "<p>Post tag-island</p>" );
</cfscript>
As you can see, using the triple back-tick allows us to embed plain HTML directly within our CFScript
block. And, when we run the above code, we get the following page output:
Pre tag-island
In tag-island
Post tag-island
That's kind of snazzy!
Now, it's worth mentioning that ColdFusion strings have always supported a multi-line syntax. Meaning, I could write the above code as follows:
<cfscript>
echo( "<p>Pre tag-island</p>" );
echo("
<p>
In tag-island
</p>
");
echo( "<p>Post tag-island</p>" );
</cfscript>
Notice that ColdFusion allows me to put line-breaks within my String. This gives us the same results as the Tag Island in the first example. However, there are significant differences between a multi-line string and a Tag Island:
A Tag Island isn't an "expression" and it doesn't return a value. It's a demarcation, like
CFScript
.A Tag Island doesn't require escaping of single-quotes or double-quotes.
A Tag Island has native support for CFML Tags and, especially helpful, Child Tags.
When I see how Tag Islands work, my very first thought is using it to embed CFQuery
and CFQueryParam
tags within my Script-based ColdFusion components. Historically, one of the most amazing features of ColdFusion has been the ultimate simplicity with which we can author injection-proof SQL statements. Modern ColdFusion allows us to do the same with CFScript
and queryExecute()
; however, queryExecute()
still doesn't compare with the pure elegance of the CFQuery
tag.
To see what I mean, let's look at a script-based example of a SQL Query that uses a prepared statement and a single binding:
<cfscript>
dump( getUserById( 1 ) );
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
public array function getUserById( required numeric userID ) {
var results = queryExecute(
sql = "
/* DEBUG: getUserById(). */
SELECT
u.id,
u.name,
u.email
FROM
user u
WHERE
u.id = :userID
;
",
params = {
userID: {
value: userID,
sqlType: "integer"
}
},
options = {
datasource: "testing",
returnType: "array"
}
);
if ( ! results.len() ) {
throw(
type = "NotFound",
message = "User not found",
detail = "User with id [#userID#] could not be found."
);
}
return( results );
}
</cfscript>
Here, we're using ColdFusion's multi-line String support to define our sql
property. And, this works fine. But, at least for me, it doesn't match the simplicity and elegance of the CFQuery
tag; which, we can now use in CFScript
with the Tag Island:
<cfscript>
dump( getUserById( 1 ) );
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
public array function getUserById( required numeric userID ) {
// NOTE: Historically, I've always authored by "Data Access Layer" (DAL) using
// tags so that I could use the CFQuery tag. I know that I can use queryExeucte()
// these days; but, it's still not quite as easy as writing plain CFQuery and
// CFQueryParam tags.
```
<cfquery name="local.results" datasource="testing" returntype="array">
/* DEBUG: getUserById(). */
SELECT
u.id,
u.name,
u.email
FROM
user u
WHERE
u.id = <cfqueryparam value="#userID#" sqltype="integer" />
;
</cfquery>
```
if ( ! results.len() ) {
throw(
type = "NotFound",
message = "User not found",
detail = "User with id [#userID#] could not be found."
);
}
return( results );
}
</cfscript>
This code gives us the same exact result as the all-script example. But, at least for me, this Tag Island version is easier on the eyes. This might be because I have close to two-decades of experience reading tag-based SQL, so that's where my muscle-memory is? Or, it could be the fact that the Tag Island approach uses a more concise syntax that places the query bindings closer to their point of consumption?
Your mileage may vary.
Another place that Tag Islands may be useful is in the construction of content buffers via CFSaveContent
. As one example, you might use content buffers to prepare the HTML and Plain Text portions of the CFMail
tag:
<cfscript>
sendMail( "Let's talk about puppies!", "puppies" );
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
public void function sendMail(
required string subject,
required string topic
) {
// NOTE: Historically, I would accomplish this same thing by putting the CFMail
// content in a .CFM file which I would then "include" into a saveContent buffer.
// This demo makes it seem feasible to include as a tag-island; however, in
// reality, HTML MARKUP FOR EMAILS IS VERY NASTY - as such, other than the most
// trivial emails, there's no way I would want to mix the CFMail markup alongside
// anything else.
```
<cfoutput>
<cfsavecontent variable="local.htmlContent">
<p>
Dear Kim,
</p>
<p>
I would like to talk to you about #encodeForHtml( topic )#. Do you
have time later this week to discuss?
</p>
</cfsavecontent>
<cfsavecontent variable="local.textContent">
Dear Kim,
I would like to talk to you about #encodeForHtml( topic )#. Do you have time later this week to discuss?
</cfsavecontent>
</cfoutput>
```
mail
to = "kim@bennadel.com"
from = "ben@bennadel.com"
subject = subject
type = "html"
async = false
{
mailPart type = "text/html" {
echo( htmlContent.reReplace( "(?m)^\t{4}", "", "all" ).trim() );
}
mailPart type = "text/plain" {
echo( textContent.reReplace( "(?m)^\t{4}", "", "all" ).trim() );
}
}
}
</cfscript>
As you can see, the Tag Island allows us to use the tag-based version of the CFSaveContent
tag, along with a bunch of native HTML syntax.
Now, it merits mentioning that you could do the same thing with savecontent
tag and the echo()
function:
<cfscript>
// ....
savecontent variable="local.htmlContent" {
echo("
<p>
Dear Kim,
</p>
<p>
I would like to talk to you about #encodeForHtml( topic )#. Do you
have time later this week to discuss?
</p>
");
}
savecontent variable="local.textContent" {
echo("
Dear Kim,
I would like to talk to you about #encodeForHtml( topic )#. Do you have time later this week to discuss?
");
}
// ....
</cfscript>
But, what if you content buffer had embedded single and double quotes? Then, suddenly you have to start escaping values. And, if your content needed some sort of embedded CFML tags, then you're probably in trouble and you have to start breaking it apart and re-constructing it in parts.
Consider a special kind content buffer, the CFXML
tag - the CFXML
tag is essentially a glorified CFSaveContent
tag that automatically parses the content buffer into an XML document at the end. With Tag Islands, we can use embed the CFXML
tag right in our script-based components:
<cfscript>
friends = [
{ id: 1, name: "Kim" },
{ id: 2, name: "Arnold" },
{ id: 3, name: "Tina" },
{ id: 4, name: "Libby" }
];
saveFriends( friends );
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
public void function saveFriends( required array friends ) {
// Building XML documents "by hand" is a horrible experience. Using a tag-island
// to construct an XML document could be a feasible "win". Though, to be honest,
// I almost never create XML documents these days (JSON for the win!).
```
<cfoutput>
<cfxml variable="local.body">
<Friends>
<cfloop index="friend" array="#friends#">
<Friend
id="#encodeForXmlAttribute( friend.id )#"
name="#encodeForXmlAttribute( friend.name )#"
/>
</cfloop>
</Friends>
</cfxml>
</cfoutput>
```
http
result = "local.results"
method = "POST"
url = "http://127.0.0.1:52116/some-xml-api-end-point/"
timeout = 2
{
httpParam
type = "header"
name = "Content-Type"
value = "application/xml"
;
httpParam
type = "body"
value = toString( body )
;
}
if ( ! results.statusCode.find( "2\d\d" ) ) {
throw(
type = "ApiFailure",
message = "XML post failed",
detail = "XML post returned with non-200 status code [#results.statusCode#].",
extendedInfo = "File content: #results.fileContent#"
);
}
}
</cfscript>
As you can see here, we're using the CFXML
tag directly within our script-based CFML in order to construct a payload for an XML-based API (which, despite the prevalence of JSON, still exist). I am sure you could create the same XML structure with savecontent
and various echo()
calls and a for-in loop. But, I am almost certain it wouldn't be as easy to write, read, and maintain as just using the CFXML
tag directly.
These days, I write most of my CFML code using Script. The major exceptions being anything View related, which I construct with HTML and embedded CFML tags; and, my Data Access Objects (DAOs), which I still write with tags so that I can use the CFQuery
and CFQueryParam
tags. So, for me personally, the huge win for Tag Islands in Lucee CFML will be the ability to write Script-based DAOs without having to give-up the simplicity, elegance, and readability of the CFQuery
and CFQueryParam
tags.
Want to use code from this post? Check out the license.
Reader Comments
This is so cool and (elegantly) solves a common frustration of mine. Thanks (so much) for pointing this out!
@Chris,
Very cool -- I'm glad this struck a chord. I wish I could start using the
CFQuery
one today -- unfortunately, at work, I'm still on Lucee CFML 5.2.x :( . But, maybe this is a good reason for us to make the push to upgrade!So cool! I resigned myself to the "in-elegance" of queries within script a few years back when I went full-script so this is huge! I'm going to try this out asap.
Ben, I can't believe you didn't test this...
Can you put a CFScript block inside of a tag island... and a tag island in that script block... and a cfscript block in that tag island... and so on, and so on?
@Adam,
Ha ha ha, good point - it does seem like something to try. Actually, I think Gavin Pickin and Brad Wood were just talking about that on the Modernize or Die podcast:
https://youtu.be/7Zcq2kTZf_I?t=1301
Gavin said that someone tested it and you can nested
cfscript
and tag-islands infinitely :D@Dan,
100% - nothing beats
CFQuery
and some easy-on-the-eyes SQL.This came up in a feed today and I found myself trawling back through the post on the Lucee dev forum - based on that, I can't believe this was ever actually implemented!
I've spent the last couple of years getting used to the
queryExecute
syntax and I'm comfortable using it, but I have to say that<cfquery>
reads better to me also... I may well start using this again. Hurrah!@Gary,
Woot woot :D
Funny, three of your main examples were sending mail, queries, and HTTP requests in script:
There's a "box" for that :)
https://www.forgebox.io/view/cbmailservices
https://www.forgebox.io/view/qb
https://www.forgebox.io/view/hyper
@Brad,
Ha ha, there's a ColdBox thing for everything :P
@All,
In the month-and-a-half since I wrote this post, I've been making heavy use of both the unified tag-syntax and tag islands in Lucee CFML, and I've come to believe this kind of makes CFScript perfect:
www.bennadel.com/blog/3793-tag-islands-and-cfscript-based-tags-bring-perfection-to-coldfusion-in-lucee-cfml-5-3-4-80.htm
I'm a little bit freaking out about it. It just feels like it brings unparalleled developer ergonomics to ColdFusion that I haven't seen in any other language (though, to be fair, my expose to other languages is fairly limited). But, it's just kind of bananas. And, I'm freaking out that more people aren't freaking out about how awesome this is!
@Ben,
Too funny! Totally agree though. Since you posted this I've used it multiple times (within query and savecontent), and agree, makes cfscript perfect! Takes the beauty and familiarity of script and mixes in a few instances where the the ease of CFML really shines.
@Dan,
RIGHT?! It's so freaking sweet! I wanna go up to people and be like, "Have you heard the good news!" :D
@All,
One place that I just used Tag Islands is when I needed to invoke a
CFImport
-based ColdFusion custom tag inside aCFScript
context:www.bennadel.com/blog/4052-using-coldfusion-custom-tags-to-create-an-html-email-dsl-in-lucee-cfml-5-3-7-47-part-xv.htm
I don't think there is any other way do this, currently, without tag islands:
You can use
CFModule
inside ofCFScript
, but that has issues in Lucee CFML, which at this time cannot usegetBaseTagData()
withCFModule
invoked tags. As such, tag islands appear to be the only way.Lucee CFML! You the bomb-diggity!