Articles
createShadowHtml UDF
Allows embedding of a full HTML document in a webpage; (ie, inline email/PDF preview)
The `createShadowHtml` CFML user-defined function (UDF) generates HTML and inline JavaScript to create a dynamic, client-side preview of a given HTML document within a shadow DOM. It optionally disables links within the preview and uses a placeholder text before the HTML is loaded. This allows for isolated rendering of HTML and CSS without interference from the main document's styles or scripts.
We use ColdFusion/CFML to generate valid HTML documents for PDF generation using jsoup & WKHTMLTOPDF. If the generated HTML content is simply outputted onto an existing webpage, the webpage becomes invalid (due to double DOCTYPE "inception") and the website's global CSS styles will polluting the preview.
In order to view the final result correctly, the HTML document would have to be viewed independently within a new tab (using `window.open` or `target="_blank"`) or embedded in an iFrame.
Another approach that we've been experimenting with uses the browser's shadow DOM. This has worked well for our needs so far, but is pretty specific... it generates a standalone shadow DOM and populates it with the content within an existing webpage so it can be more faithfully rendered without any interactivity. Our PDF & email previews are now rendered inline and are much easier to review.
Source Code
<cfscript>
string function createShadowHtml(required string html, string id="", boolean enableLinks=true, string placeholder="Previewable content will be displayed here...") hint="I generate a shadow DOM on-the-fly to preview HTML/CSS (No JS)" {
local.newLine = createobject("java", "java.lang.System").getProperty("line.separator");
local.id = (isvalid("variablename", arguments.id)) ? arguments.id : "shadow" & randrange(10000,99999,"SHA1PRNG");
local.html = ["<div id=""#local.id#"">#trim(arguments.placeholder)#</div>"];
arrayappend(local.html, "<script>");
arrayappend(local.html, "var pane_#local.id# = document.querySelector('###local.id#');");
arrayappend(local.html, "var shadow_#local.id# = pane_#local.id#.attachShadow({mode: 'open'});");
arrayappend(local.html, "var html_#local.id# = #serializejson({"html": trim(arguments.html)})#");
arrayappend(local.html, "shadow_#local.id#.innerHTML = html_#local.id#.html;");
if (!arguments.enableLinks){
arrayappend(local.html, "shadowLinks_#local.id# = shadow_#local.id#.querySelectorAll('a');");
arrayappend(local.html, "for (link of shadowLinks_#local.id#){");
arrayappend(local.html, "link.addEventListener('click', function(e){");
arrayappend(local.html, "e.preventDefault();");
arrayappend(local.html, "alert('Sorry. Links aren\'t enabled in preview mode.');");
arrayappend(local.html, "});");
arrayappend(local.html, "}");
}
arrayappend(local.html, "</script>");
return arraytolist(local.html, local.newLine);
}
</cfscript>
Demo
<cfsavecontent variable="emailHtml">
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8">
<style>body, h1, p, a {color:red !important;}</style>
</head>
<body>
<h1>Hello World</h1>
<a href="/unsubscribe/">Unsubscribe</a>
</body>
</html>
</cfsavecontent>
<cfset htmlPreview = createShadowHtml(html=emailHtml, enableLinks=false)>
<cfoutput>
<div style="border:1px solid black; transform: scale(0.7);">#htmlPreview#</div>
<p><b>NOTE:</b> The body <p> text is not affected by the CSS rule that makes text red.</p>
<h2>Shadow DOM HTML Source:</h2>
<pre>#encodeforhtml(emailHtml)#</pre>
</cfoutput>