Using ColdFusion Custom Tags To Create An HTML Email DSL In Lucee CFML 5.3.7.47, Part IV
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:
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
@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.