Using ColdFusion Custom Tags To Create An HTML Email DSL In Lucee CFML 5.3.7.47, Part XI
In an earlier post, I looked at several encapsulation techniques that I can use in my ColdFusion custom tag DSL (Domain Specific Language) for HTML emails. A technique like "attribute passing" works well when you are passing data one layer down. However, as HTML email layouts become more complex, sometimes you need to make data accessible several layers down in your custom tag DOM (Document Object Model). In order to avoid so-called "prop drilling" (a term used in the React.js world), I wanted to borrow a concept from Angular: Providers. In my DSL, a "provider" is just a key-value pair defined at a high-level that can then be referenced at a lower-level in your HTML email markup.
View this code in my ColdFusion Custom Tag Emails project on GitHub.
To see how this is helpful, imagine an HTML email structure that looks like this:
Email → Body → Footer → Links
In this structure, the "Links" construct needs a set of URLs in order to render the underlying Anchor tags. And, if I was using "attributes" to pass data, I'd have to pass them into the Body; then, the Body would have to pass them into the Footer. This is unnecessarily verbose.
By using a Provider instead, I can have the Email define a set of Links at the top; and then, the Links in the footer can just "reach up" and grab those values. No more "prop drilling" to get data deep into the custom tag DOM.
To see this in action, I've created an example that uses the structure outlined above:
<!--- Import custom tag libraries. --->
<cfimport prefix="core" taglib="./core/" />
<cfimport prefix="html" taglib="./core/html/" />
<cfimport prefix="ex12" taglib="./ex12/" />
<!--- // ------------------------------------------------------------------------- // --->
<!--- // ------------------------------------------------------------------------- // --->
<core:Email
subject="Providing values"
teaser="Borrowing more inversion of control ideas from Angular!">
<!---
The Provide tag sets up key-value pairs that are accessible to other ColdFusion
custom tags in the Email. This is just another way to provide data to lower-level
rendering abstractions. This approach will be useful for deeply-nested tags that
would otherwise require "prop drilling" in order to get data down several layers
of rendering. For example:
<ex12:Body> => <ex12:FooterLinks> => <html:a>
In order to get URLs down to the "footer links" component WITHOUT having to first
provide them to the "body" component as an intermediary, we can "provide" them
and then the "footer links" component can just reach for them directly.
--->
<core:Provide name="siteUrl" value="https://www.bennadel.com/" />
<core:Provide name="aboutUrl" value="https://www.bennadel.com/about" />
<core:Provide name="peopleUrl" value="https://www.bennadel.com/people" />
<ex12:Body>
<html:h1>
Providing deep values
</html:h1>
<html:p>
In <html:mark>Example 8</html:mark>, we looked at various ways to encapsulate
rendering details:
</html:p>
<html:ul>
<html:li>Using CFInclude.</html:li>
<html:li>Using tag attributes.</html:li>
<html:li>Using tag generated content.</html:li>
<html:li>Using multi-slot projection.</html:li>
</html:ul>
<html:p>
Now, I'd like to borrow <html:em>yet another</html:em> idea from Angular:
<html:mark>Providers</html:mark>. A provider just creates a key-value pair
that is subsequently accessible to every other ColdFusion custom tag in
the email rendering.
</html:p>
</ex12:Body>
</core:Email>
As you can see, in between the <core:Email>
tag and the <ex12:Body>
tag, I have several <core:Provide>
tags. These are simple tags that just define a key-value pair. Under the hood, all the Provide
tag is doing is reaching up into the root Email
tag and populated a .providers
struct:
<cfscript>
// Define custom tag attributes.
param name="attributes.name" type="string";
param name="attributes.value" type="any";
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
switch ( thistag.executionMode ) {
case "start":
getBaseTagData( "cf_email" ).providers[ attributes.name ] = attributes.value;
// Make sure this tag has NO BODY.
exit method = "exitTag";
break;
}
</cfscript>
As you can see, it's just setting a key-value pair into the .providers
struct.
To see how this is then consumed, here's a truncated version of the Body
tag:
<cfswitch expression="#thistag.executionMode#">
<cfcase value="end">
<cfoutput>
<cfset email = getBaseTagData( "cf_email" ) />
<!---
In order to access the defined providers, I just have to reach up into
the base "email" tag and grab the Providers struct. The same way I do
for the theme data.
--->
<cfset theme = email.theme />
<cfset providers = email.providers />
<!--- ... truncated ... --->
<html:table width="#theme.width#" class="ex12-body">
<html:tr>
<html:td colspan="3" class="ex12-body-top-border">
<br />
</html:td>
</html:tr>
<html:tr>
<html:td width="60" class="ex12-body-gutter">
<br />
</html:td>
<html:td class="ex12-body-content">
#thistag.generatedContent#
</html:td>
<html:td width="60" class="ex12-body-gutter">
<br />
</html:td>
</html:tr>
<html:tr>
<html:td colspan="3" align="center" class="ex12-body-footer">
Questions?
<html:a href="#providers.siteUrl#">I'm here to help.</html:a>
</html:td>
</html:tr>
</html:table>
<ex12:FooterLinks />
<!--- Reset the generated content since we're overriding the output. --->
<cfset thistag.generatedContent = "" />
</cfoutput>
</cfcase>
</cfswitch>
As you can see, this Body
tag includes:
<ex12:FooterLinks />
... which is defined as follows:
<!--- Import custom tag libraries. --->
<cfimport prefix="core" taglib="../core/" />
<cfimport prefix="html" taglib="../core/html/" />
<!--- // ------------------------------------------------------------------------- // --->
<!--- // ------------------------------------------------------------------------- // --->
<cfswitch expression="#thistag.executionMode#">
<cfcase value="start">
<cfoutput>
<cfset email = getBaseTagData( "cf_email" ) />
<!---
In order to access the defined providers, I just have to reach up into
the base "email" tag and grab the Providers struct. The same way I do
for the theme data.
--->
<cfset theme = email.theme />
<cfset providers = email.providers />
<core:HtmlEntityTheme entity="p">
color: ##999999 ;
font-size: 14px ;
line-height: 19px ;
text-align: center ;
</core:HtmlEntityTheme>
<core:HtmlEntityTheme entity="a">
color: ##999999 ;
padding: 0px 3px 0px 3px ;
</core:HtmlEntityTheme>
<core:HeaderStyles>
.secondary-footer a:hover {
background-color: #theme.light.primary# ;
color: ##ffffff ;
}
</core:HeaderStyles>
<html:p margins="large none" class="secondary-footer">
<html:a href="#providers.siteUrl#">Ben Nadel</html:a>
<html:span> | </html:span>
<html:a href="#providers.aboutUrl#">About</html:a>
<html:span> | </html:span>
<html:a href="#providers.peopleUrl#">Amazing People</html:a>
</html:p>
<!--- Make sure this tag has NO BODY. --->
<cfexit method="exitTag" />
</cfoutput>
</cfcase>
</cfswitch>
As you can see, both the Body
tag and the FooterLinks
tag just reach up in to the base Email
tag and grab the .providers
collection. Values can then be plucked out of this collection for rendering as needed - no need to pass values down through ColdFusion custom tag attributes just for the sake of providing them deeper in the DOM.
Now, if we render this in the browser, we get the following output:
As you can see, the Provide
tag we defined at the top-level of the HTML email:
<core:Provide name="siteUrl" value="..." />
... was successfully accessed in order to render an Anchor tag deep within the HTML DOM structure.
To be clear, the Provide
tag is not intended to replace other data-passing and encapsulation techniques - it's meant to be an additional approach that can be used as email layouts become more deeply nested. Using ColdFusion custom tag attributes is nice because it is very explicit. Providers, on the other hand, step slightly in the direction of "magical". But, given the fact that the writing-to and reading-from the .providers
collection is very clear in the tag syntax, I think it's still quite manageable.
Want to use code from this post? Check out the license.
Reader Comments