Skip to main content
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: James Brooks
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: James Brooks

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

By on
Tags:

In the first post on my ColdFusion custom tag DSL (Domain Specific Language) for HTML emails, I had an image-grid abstraction that took a list of images and rendered a responsive table. In that example, I had several custom tags that did nothing but aggregate data (ie, they had no layout in and of themselves). As I've continued to think about more complex layouts, I've noticed this pattern cropping back up. As such, I thought it would be worth codifying in a core tag, <core:Slot>. The Slot tag is meant for content projection in which the rendered contents of the Slot tag are just captured and passed back up to an ancestor tag such that they can be subsumed into more complex layouts.

View this code in my ColdFusion Custom Tag Emails project on GitHub.

The <core:Slot> tag must have a name attribute. This is the name of the property that will be set into an ancestor tag. Under the hood, the <core:Slot> tag will keep walking up the ColdFusion custom tag hierarchy until it finds a tag with a slots property. At that point, it will store the rendered content of the <core:Slot> tag into the .slots[name] value.

Optionally, the <core:Slot> tag can use multi="true". This will cause the <core:Slot> tag to treat the .slots[name] value as an Array; and, will call .append() on it rather than overwriting the value with a direct assignment.

To see this in action, I've put together a demo that uses both the default and the multi="true" version of the Slot concept. The first re-creates a responsive image grid and the second is just a link-bar.

<!--- Import custom tag libraries. --->
<cfimport prefix="core" taglib="./core/" />
<cfimport prefix="ex7" taglib="./ex7/" />
<cfimport prefix="html" taglib="./core/html/" />

<!--- // ------------------------------------------------------------------------- // --->
<!--- // ------------------------------------------------------------------------- // --->

<core:Email
	subject="Content projection slots"
	teaser="Making complex layouts easier to reason about!">
	<ex7:Body>

		<html:h1>
			Using content projection slots for layout
		</html:h1>

		<html:p>
			When layouts get more complicated, we can try to keep the HTML simple by
			separating the <html:strong>definition of the content</html:strong> from the
			<html:strong>layout of the content</html:strong>. To do this, we can create
			"slots" that aggregate data into variables that a ColdFusion custom tag can
			then use within an <html:mark>encapsulated layout</html:mark>.
		</html:p>

		<ex7:ImageGrid>
			<!---
				The ImageGrid component serves two purposes: first, it can provide themes
				to the child content; and second, it defines a "slots" object that can be
				used for content project. In this case, all of the image rendering is
				being collected into a multi-slot (Array) called "images". The ImageGrid
				will then use that "images" array to render the underlying TALBE tag(s).
				--
				NOTE: I'm using maths for the height since these are not the natural
				dimensions of the image.
			--->
			<core:Slot name="images" multi="true">
				<html:img
					src="https://bennadel-cdn.com/images/header/photos/jeremiah_lee_2.jpg"
					alt="Ben Nadel and Jeremiah Lee, double-front biceps!"
					width="225"
					height="#round( 225 / 1120 * 570 )#"
				/>
			</core:Slot>
			<core:Slot name="images" multi="true">
				<html:img
					src="https://bennadel-cdn.com/images/header/photos/jeremiah_lee_2.jpg"
					alt="Ben Nadel and Jeremiah Lee, double-front biceps!"
					width="225"
					height="#round( 225 / 1120 * 570 )#"
				/>
			</core:Slot>
			<core:Slot name="images" multi="true">
				<html:img
					src="https://bennadel-cdn.com/images/header/photos/jeremiah_lee_2.jpg"
					alt="Ben Nadel and Jeremiah Lee, double-front biceps!"
					width="225"
					height="#round( 225 / 1120 * 570 )#"
				/>
			</core:Slot>
			<core:Slot name="images" multi="true">
				<html:img
					src="https://bennadel-cdn.com/images/header/photos/jeremiah_lee_2.jpg"
					alt="Ben Nadel and Jeremiah Lee, double-front biceps!"
					width="225"
					height="#round( 225 / 1120 * 570 )#"
				/>
			</core:Slot>
		</ex7:ImageGrid>

		<html:p>
			In this case, the <html:strong>&lt;core:ImageGrid&gt; tag</html:strong> is
			proving both a Desktop and a Mobile view!
		</html:p>

		<html:p>
			Now, the ImageGrid component slots used "multi", which means the slot was
			treated as an Array. But, the default behavior for a slot is just to set a
			single variable value.
		</html:p>

		<!---
			The Links ColdFusion custom tag has two slots: "left" and "right". The
			following Slot tags simply assign the generated content to those values.
		--->
		<ex7:Links>
			<core:Slot name="left">
				<html:a href="https://www.bennadel.com/">BenNadel.com</html:a> &rarr;
			</core:Slot>
			<core:Slot name="right">
				<html:a href="https://www.bennadel.com/people/">People</html:a> &rarr;
			</core:Slot>
		</ex7:Links>

		<html:p margins="none">
			This is gonna be hella sweet, I think!
		</html:p>

	</ex7:Body>
</core:Email>

As you can see, there's a ColdFusion custom tag, <ex7:ImageGrid>, that has four Slots, all called, images. Each of the <img> tags will be aggregate into the .slots[images] property as an Array.

The <ex7:Links> ColdFusion custom tag then has two uniquely named slots, left and right. These will be stored into the .slots.left and .slots.right properties, respectively.

Here's the code for the ImageGrid - notice that the start mode of the ColdFusion custom tag must defined the .slots property. And, in this case, the start mode is also providing some local theming:

<!--- Import custom tag libraries. --->
<cfimport prefix="core" taglib="../core/" />
<cfimport prefix="html" taglib="../core/html/" />

<!--- // ------------------------------------------------------------------------- // --->
<!--- // ------------------------------------------------------------------------- // --->

<cfswitch expression="#thistag.executionMode#">
	<cfcase value="start">
		<cfoutput>

			<cfset slots = {
				images: []
			} />

			<core:HtmlEntityTheme entity="img">
				border-radius: 4px 4px 4px 4px ;
			</core:HtmlEntityTheme>

		</cfoutput>
	</cfcase>
	<cfcase value="end">
		<cfoutput>

			<core:HtmlEntityTheme entity="td">
				padding: 7px 7px 7px 7px ;
			</core:HtmlEntityTheme>

			<core:IfDesktopView>

				<html:table width="100%" margins="none small">
				<html:tr>
					<html:td align="center" class="html-entity-line-height-reset">
						#slots.images[ 1 ]#
					</html:td>
					<html:td align="center" class="html-entity-line-height-reset">
						#slots.images[ 2 ]#
					</html:td>
				</html:tr>
				<html:tr>
					<html:td align="center" class="html-entity-line-height-reset">
						#slots.images[ 3 ]#
					</html:td>
					<html:td align="center" class="html-entity-line-height-reset">
						#slots.images[ 4 ]#
					</html:td>
				</html:tr>
				</html:table>

			</core:IfDesktopView>

			<core:IfMobileView>

				<core:MaxWidthStyles>
					.ex7-image-grid img {
						height: auto ;
						width: 100% ;
					}
				</core:MaxWidthStyles>

				<html:table width="100%" margins="none small" class="ex7-image-grid">
				<html:tr>
					<html:td align="center img-line-height-reset">
						#slots.images[ 1 ]#
					</html:td>
				</html:tr>
				<html:tr>
					<html:td align="center img-line-height-reset">
						#slots.images[ 2 ]#
					</html:td>
				</html:tr>
				<html:tr>
					<html:td align="center img-line-height-reset">
						#slots.images[ 3 ]#
					</html:td>
				</html:tr>
				<html:tr>
					<html:td align="center img-line-height-reset">
						#slots.images[ 4 ]#
					</html:td>
				</html:tr>
				</html:table>

			</core:IfMobileView>

			<!--- Reset the generated content since we're overriding the output. --->
			<cfset thistag.generatedContent = "" />

		</cfoutput>
	</cfcase>
</cfswitch>

As you can see, once the .slots.images property is populated by the child Slots, we can then references the value in the end mode of the tag.

Here's the non-multi implementation of the Links tag:

<!--- Import custom tag libraries. --->
<cfimport prefix="core" taglib="../core/" />
<cfimport prefix="html" taglib="../core/html/" />

<!--- // ------------------------------------------------------------------------- // --->
<!--- // ------------------------------------------------------------------------- // --->

<cfswitch expression="#thistag.executionMode#">
	<cfcase value="start">
		<cfoutput>

			<cfset slots = {
				left: "",
				right: ""
			} />

			<core:HtmlEntityTheme entity="a">
				color: ##ff3366 ;
			</core:HtmlEntityTheme>

		</cfoutput>
	</cfcase>
	<cfcase value="end">
		<cfoutput>

			<core:HtmlEntityTheme entity="td">
				white-space: nowrap ;
			</core:HtmlEntityTheme>

			<html:table width="100%">
			<html:tr>
				<html:td width="50%" align="center">
					#slots.left#
				</html:td>
				<html:td width="50%" align="center">
					#slots.right#
				</html:td>
			</html:tr>
			</html:table>

			<!--- Reset the generated content since we're overriding the output. --->
			<cfset thistag.generatedContent = "" />

		</cfoutput>
	</cfcase>
</cfswitch>

As you can see, the concept is very similar. The only difference is, instead of treating one Slot variable like an Array, we're writing to and reading from two distinct variables.

Now, if we rendering this HTML email layout in a desktop view, we get the following layout:

An HTML email built with ColdFusion custom tags.

As you can see, the four image slots were cleanly translated into a 2-by-2 grid. And, since the <ex7:ImageGrid> ColdFusion custom tag also provided a responsive layout, this is what we get when we render this is a mobile device simulation:

An HTML email built with ColdFusion custom tags and rendered in a mobile simulation showing the responsive nature of the image grid.

This is really exciting! I look at the HTML / CFML code for the example and it feels like it strikes a beautiful balance between abstraction, complexity, and elegance. This content project concept - which I borrowed from Angular - is going to create a lot of flexibility when it comes to more advanced layouts.

Want to use code from this post? Check out the license.

Reader Comments

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel