Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

Using ColdFusion Custom Tags To Create An HTML Email DSL In Lucee CFML 5.3.7.47, Part XI

By Ben Nadel on
Tags: ColdFusion

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="www.bennadel.com/" />
	<core:Provide name="aboutUrl" value="www.bennadel.com/about" />
	<core:Provide name="peopleUrl" value="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>&nbsp;|&nbsp;</html:span>
				<html:a href="#providers.aboutUrl#">About</html:a>
				<html:span>&nbsp;|&nbsp;</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:

Key-value pairs being provided in order to render anchors deep within an HTML email DOM structure.

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.



Reader Comments

What has two thumbs and hopes you leave a comment? This Guy! (Ben Nadel).

Post A Comment

You — Get Out Of My Dreams, Get Into My Blog
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.