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 IV

By Ben Nadel on
Tags: ColdFusion

When creating dynamic HTML emails, you can progressively enhance a layout (on more modern devices) using CSS media queries. However, doing so requires repetitive boilerplate: wrapping the styles in a @media block and - super importantly - remembering to append the !important flag to every single CSS property in your style block. Since I plan to use CSS media queries in my ColdFusion custom tag DSL (Domain Specific Language) for HTML emails, I wanted to find a way to abstract the boilerplate.

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

What I ended up creating was a two-tier abstraction. At the lowest layer, I added a <core:MediaQueryStyles> tag which adds the @media block and injects the !important flag. Then, one tier above that, I created <core:MaxWidthStyles> and <core:MinWidthStyles> which further encapsulates some of the cruft.

To see this in action, here's a simple layout in which I'm dynamically changing the background color of a paragraph on different width screens:

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

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

<core:Email
	subject="Playing with media queries"
	teaser="Max-widths, Min-widths, oh heck yeah!">
	<core:Body>

		<html:h1 margins="none xlarge">
			Playing with media queries
		</html:h1>

		<core:HtmlEntityTheme entity="p" class="box">
			background-color: #f0f0f0 ;
			padding: 30px 10px 30px 10px ;
			text-align: center ;
		</core:HtmlEntityTheme>

		<html:p class="box">
			Media query styles!
		</html:p>

		<!---
			The MaxWidthStyles and MinWidthStyles are encapsulations of the CSS media-
			query. The "!important" flag is auto-injected so that you don't have to worry
			about always adding it.
		--->
		<core:MaxWidthStyles width="650">
			.box {
				background-color: #d0d0d0 ;
			}
		</core:MaxWidthStyles>
		<core:MaxWidthStyles width="600">
			.box {
				background-color: #c0c0c0 ;
			}
		</core:MaxWidthStyles>
		<core:MaxWidthStyles width="550">
			.box {
				background-color: #a0a0a0 ;
			}
		</core:MaxWidthStyles>
		<core:MaxWidthStyles width="500">
			.box {
				background-color: #909090 ;
				color: #ffffff ;
			}
		</core:MaxWidthStyles>
		<core:MaxWidthStyles width="450">
			.box {
				background-color: #707070 ;
				color: #ffffff ;
			}
		</core:MaxWidthStyles>
		<core:MaxWidthStyles width="400">
			.box {
				background-color: #505050 ;
				color: #ffffff ;
			}
		</core:MaxWidthStyles>
		<core:MaxWidthStyles width="350">
			.box {
				background-color: #303030 ;
				color: #ffffff ;
			}
		</core:MaxWidthStyles>

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

ASIDE: Adding padding to a <p> tag is not safe for an HTML email as it will not render properly in all devices. I am using it here just to keep the demo as simple as possible.

Notice that I am providing a width to my <core:MaxWidthStyles> tags. This is optional. If I didn't provide it, the tag would default to using the theme.width value (which we'll see in a second). But, more importantly, notice that I am not providing the @media syntax or the !important flag - that's all happening automatically.

Now, if we render this layout in the browser and resize the window, we get the following dynamic styling:

CSS being applied dynamically as screen resizes.

As you can see, the CSS properties within my <core:MaxWidthStyles> tags are being dynamically applied as I resize the browser.

Let's take a quick look at what is happening under the hood. Here's the code for my <core:MaxWidthStyles> ColdFusion custom tag:

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

<!--- Define custom tag attributes. --->
<cfparam name="attributes.injectImportant" type="boolean" default="true" />
<cfparam name="attributes.width" type="numeric" default="0" />

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

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

			<cfset theme = getBaseTagData( "cf_email" ).theme />
			<cfset width = ( attributes.width ? attributes.width : theme.width ) />

			<core:MediaQueryStyles
				name="max-width"
				value="#width#px"
				injectImportant="#attributes.injectImportant#">
				#thistag.generatedContent#
			</core:MediaQueryStyles>

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

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

As you can see, it's just a thin layer above the <core:MediaQueryStyles> ColdFusion custom tag. That's where the real magic happens:

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

<!--- Define custom tag attributes. --->
<cfparam name="attributes.name" type="string" />
<cfparam name="attributes.value" type="string" />
<cfparam name="attributes.injectImportant" type="boolean" default="true" />

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

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

			<core:HeaderStyles>
				@media only screen and ( #attributes.name#: #attributes.value# ) {

					#prepareStyles( thistag.generatedContent, attributes.injectImportant )#

				}
			</core:HeaderStyles>

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

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

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

<cfscript>

	/**
	* I (optionally) inject the "!important" flag at the end of each CSS property line,
	* using the semi-colon as a hook into the placement.
	* 
	* @content I am the style content being augmented.
	*/
	public string function prepareStyles(
		required string content,
		required boolean injectImportLineFlag
		)
		cachedWithin = "request"
		{

		if ( ! injectImportLineFlag ) {

			return( content );

		}

		if ( content.findNoCase( "!important" ) ) {

			throw(
				type = "UnexpectedImportant",
				message = "MediaQueryStyles cannot contain !important if it is also being injected.",
				extendedInfo = "Content: #content#"
			);

		}

		return( content.reReplace( "(?m)(;[ \t]*$)", " !important \1", "all" ) );

	}

</cfscript>

This tag is, in and of itself, really just an abstraction of the <core:HeaderStyles> tag, which appends CSS style blocks to the HTML <head> tag of the rendered email. The big value-add here is that it wraps the CSS content in the @media block and auto-injects the !important flag using Regular Expressions (RegEx).

The whole goal of the ColdFusion custom tag DSL for HTML emails is to make writing HTML emails easier. Which, for me, means abstracting away a lot of the complexity like inlining of styles and layouts. No longer having to worry about CSS media queries is now just one more thing I won't have to worry about.



Reader Comments

@All,

Building on top of the <core:MediaQueryStyles> in this post, I wanted to see if I could add some support for Dark Mode:

www.bennadel.com/blog/3983-using-coldfusion-custom-tags-to-create-an-html-email-dsl-in-lucee-cfml-5-3-7-47-part-v.htm

It uses the same exactly approach as <core:MaxWidthStyles>: a thin wrapper around the underlying MediaQueryStyles tag. Plus, some additional information that I have to inject into the <head> tag.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Blog
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
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.