Skip to main content
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Ted Steinman and Tim Meyer and Andy Pittman
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Ted Steinman Tim Meyer Andy Pittman

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

By
Published in

For the last couple of weeks, I've been neck-deep in HTML emails, fleshing out a ColdFusion custom tag DSL (Domain Specific Language) for HTML email authoring. In my previous posts, I've been building-up theming concepts, responsive desktop and mobile logic, CSS media query overrides, dark mode support, and multi-slot content projection. With all of these tools in my HTML email toolbox, I wanted to finally try to recreate a real-world HTML email. Specifically, one with a more advanced layout to see if the current tools provide sufficient functionality. And so, in this post, I am going to try and build the new comment email (or, my take on it), that we might send out from InVision.

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

In the past, I've said that one of my goals with these ColdFusion custom tags is to allow the semantic HTML to bubble up to the surface while the layout constructs fade into the background. Unfortunately, as the layout becomes more complex, the more the layout requirements come into the foreground.

Such is true with the "new comment" email. Due to the complexity of the layout, it contains more "abstractions" that you would normally need in a simpler email. However, the overall complexity of the email is actually not too bad. And, I found it rather straightforward to build, once I realized that I needed to add some reusable widgets.

The "new comment" email consists of the latest comment at the top followed by a call-to-action and then the full comment thread. Since layout requirements of each individual comment is somewhat complicated, I created a "Comment" abstraction that I can then reuse.

Here's the main email body:

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

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

<core:Email
	subject="Re: [People Photos] Ben and Joel [1]"
	teaser="Ben Nadel left a comment">
	<!---
		Instead of having the base styles in the BODY tag, I'm going to pull them out
		into a theme-specific include. This way, it makes it a bit more obvious as to
		where the possible class-names are coming from.
		--
		NOTE: We could have more than one include here. I imagine it might be nice to
		have a "base" theme file and then a "comments" theme file after that (for styles
		very specific to THIS email).
	--->
	<cfinclude template="./ex9/theme.cfm" />
	<ex9:Body>

		<html:h1>
			Ben Nadel just commented on
			<html:a href="https://www.invisionapp.com/" decoration="false">People Photos</html:a>
		</html:h1>

		<ex9:CommentCallout>
			<html:a href="https://www.invisionapp.com/" class="thumbnail-link">
				<html:img
					src="https://bennadel-cdn.com/images/header/photos/joel_hill_3.jpg"
					width="477"
					height="243"
					class="thumbnail"
				/>
			</html:a>

			<ex9:Comment
				userName="Ben Nadel"
				userInitials="BN"
				userAvatarUrl="https://bennadel-cdn.com/images/global/ben-nadel-avatar.jpg"
				commentCreatedAt="2021-02-12 06:45:29"
				commentText="I tried to play with the contrast a bit, but it's stil not perfect. That said, I think it's good enough for now. Agreed?"
				margins="xxlarge none"
			/>
		</ex9:CommentCallout>

		<ex9:CallToAction href="https://www.invisionapp.com/" margins="none normal">
			View comment
		</ex9:CallToAction>

		<html:p class="cta-reply-option">
			<html:span class="cta-reply-option-arrow">&larr;</html:span>
			or just reply to this email
		</html:p>

		<html:hr />

		<html:h2>
			Re: Ben and Joel
		</html:h2>

		<ex9:Comment
			userName="Ben Nadel"
			userInitials="BN"
			userAvatarUrl="https://bennadel-cdn.com/images/global/ben-nadel-avatar.jpg"
			commentCreatedAt="2021-02-10 18:34:20"
			commentText="Good sir, I finally got around to preparing all the conference photos. I'm still getting used ot taking photos with the iPhone. I'm struggling a bit with the color correction and the contrast. What do you think so far?"
		/>

		<ex9:Comment
			userName="Joel Hill"
			userInitials="JH"
			commentCreatedAt="2021-02-10 20:58:17"
			commentText="First off, it was great to see you again! That said, this photo is looking a bit washed-out to me. Maybe can you try increasing the contrast?"
		/>

		<ex9:Comment
			userName="Ben Nadel"
			userInitials="BN"
			userAvatarUrl="https://bennadel-cdn.com/images/global/ben-nadel-avatar.jpg"
			commentCreatedAt="2021-02-12 06:45:29"
			commentText="I tried to play with the contrast a bit, but it's stil not perfect. That said, I think it's good enough for now. Agreed?"
			margins="none"
		/>

	</ex9:Body>
</core:Email>

As you can see, there's not a whole lot of markup in the email body because the lower-level concepts have been abstracted behind ColdFusion custom tags. The main widget here is the "Comment", which includes an Avatar, the author name, date, and comment text:

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

<!--- Define custom tag attributes. --->
<cfparam name="attributes.commentCreatedAt" type="date" />
<cfparam name="attributes.commentText" type="string" />
<cfparam name="attributes.margins" type="string" default="none xlarge" />
<cfparam name="attributes.userAvatarUrl" type="string" default="" />
<cfparam name="attributes.userInitials" type="string" />
<cfparam name="attributes.userName" type="string" />

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

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

			<core:HtmlEntityTheme entity="td" class="avatar-container">
				padding: 0px 13px 0px 5px ;
			</core:HtmlEntityTheme>
			<core:HtmlEntityTheme entity="td" class="comment-container">
				padding: 0px 10px 0px 0px ;
			</core:HtmlEntityTheme>
			<core:MaxWidthStyles width="400">
				.avatar-container {
					padding: 0px 10px 0px 0px ;
				}

				.comment-container {
					padding: 0px 0px 0px 0px ;
				}
			</core:MaxWidthStyles>
			<core:HtmlEntityTheme entity="h3">
				font-size: 16px ;
				line-height: 22px ;
			</core:HtmlEntityTheme>
			<core:HtmlEntityTheme entity="span" class="created-at">
				color: ##999999 ;
				font-weight: 400 ;
			</core:HtmlEntityTheme>
			<core:MaxWidthStyles>
				.created-at {
					display: block ;
					font-size: 14px ;
					line-height: 17px ;
					margin-bottom: 5px ;
					margin-top: 3px ;
				}

				.created-at-dash {
					display: none ;
				}
			</core:MaxWidthStyles>
			<core:HtmlEntityTheme entity="p">
				font-size: 16px ;
				line-height: 22px ;
			</core:HtmlEntityTheme>

			<!--- NOTE: Cellpadding needed for older email clients. --->
			<html:table width="100%" cellpadding="8" margins="#attributes.margins#">
			<html:tr>
				<html:td class="avatar-container">

					<ex9:Avatar
						imageUrl="#attributes.userAvatarUrl#"
						initials="#attributes.userInitials#"
						margins="none"
					/>

				</html:td>
				<html:td width="100%" class="comment-container">

					<html:h3 margins="none xxsmall">
						#encodeForHtml( attributes.userName )#

						<html:span class="created-at">
							<html:span class="created-at-dash">&mdash;</html:span>
							#dateFormat( attributes.commentCreatedAt, "mmm d, yyyy" )# at
							#timeFormat( attributes.commentCreatedAt, "h:mm TT" )# (UTC)
						</html:span>
					</html:h3>

					<html:p margins="none">
						#attributes.commentText#
					</html:p>

				</html:td>
			</html:tr>
			</html:table>

			<!--- Make sure this tag has NO BODY. --->
			<cfexit method="exitTag" />

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

As you can see, the lower-level layout of the comment uses a two-column (table-based) layout with the Avatar on the left and the comment information on the right. Note that the Avatar itself is also a reusable ColdFusion custom tag (which we saw in a previous post on encapsulation techniques).

This Comment abstraction has its own local theming, including responsive styles that change the margins (padding) and font-sizes at small screen resolutions. I am really enjoying that the theme code and the widget code is collocated. I believe that's going to do a lot for long-term maintenance.

I won't go into too much code in this post since the last posts have all examined these concepts before. But, if we run this email through Litmus, here's what we get:

Apple Mail 12 on MacOS

Gmail App on Android 8

Gmail on Chrome

Gmail on iOS 13

IBM Notes on Windows 10

Outlook 2013 on Windows 10

Outlook 2016 on MacOS

Outlook.com in Dark Mode

Excuse my French, but these look freakin' hawt! I mean, come on, this even looks pretty solid in Outlook 2013 on Windows, which is normally the kryptonite of HTML email layouts!

I'm really excited to start using this approach in a production environment. There's definitely some more polish to add; but, I think these ColdFusion custom tags have demonstrated that they are ready for real-world use. I can't tell you how much a joy Lucee CFML is to work with! It just brightens my whole day.

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