Using ColdFusion Custom Tags To Create An HTML Email DSL In Lucee CFML 5.3.7.47, Part VI
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><core:ImageGrid> 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> →
</core:Slot>
<core:Slot name="right">
<html:a href="https://www.bennadel.com/people/">People</html:a> →
</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:
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:
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