Skip to main content
Ben Nadel at BFusion / BFLEX 2010 (Bloomington, Indiana) with: Matt Boles
Ben Nadel at BFusion / BFLEX 2010 (Bloomington, Indiana) with: Matt Boles

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

By
Published in Comments (1)

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.

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