Skip to main content
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: Dan Sirucek
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: Dan Sirucek@dsirucek )

Caveat When Using Umbrella JS With Template Elements In JavaScript

By on

The other day, when generating PDF document signatures with html2canvas, I was using a <template> element to stamp-out DOM-element clones within my JavaScript application. Those clones were each subsequently wrapped in an Umbrella JS instance for easy DOM-manipulation. However, I ran into some quirkiness if I tried to .appendTo() the clone to the document body before I was done manipulating it. It seemed that none of the API calls that I was making to the Umbrella JS instance were being applied to the clone once the template clone was rendered to the DOM.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

To be honest, it's been a looong time since I've worked directly with a DocumentFragment. Most of my work these days is done in AngularJS and Angular, which provides HTML directives that, more or less, obviate the need for DocumentFragment usage. As such, the rules around fragments have slowly left my head.

When I was working on the demo the other day, I was thinking about a fragment like it was a collection of arbitrary references. Somewhat akin to an Array of DOM elements. But, that's not what it is at all - it's a light-weight DOM tree. This is a critical distinction because any given DOM node can only exist in one place within a DOM tree at one time. As such, by adding the contents of a fragment to the body, we are implicitly removing the contents from the fragment.

Again, any given DOM node can only exist in one part of the DOM at a time.

This behavior is explicitly outlined in the DocumentFragment documentation on MDN:

A common use for DocumentFragment is to create one, assemble a DOM subtree within it, then append or insert the fragment into the DOM using Node interface methods such as appendChild() or insertBefore(). Doing this moves the fragment's nodes into the DOM, leaving behind an empty DocumentFragment.

And, this is exactly what was happening to me. Only, since I was using Umbrella JS to access the DOM, it wasn't immediately clear to me that the fragment was suddenly empty. That's because an Umbrella JS instance - like a jQuery instance - will happily work with an empty collection, turning all API methods into no-ops.

Let's see this behavior in action. In the following JavaScript code, I'm going to clone a <template> and then inject it into the DOM. It contains a single paragraph tag; and, I'm going to try to adjust the textContent both before and after the injection:

<!doctype html>
<html lang="en">
<head>
	<meta charset="utf-8" />
</head>
<body>

	<h1>
		Caveat When Using Umbrella JS With Template Elements
	</h1>

	<template>
		<p>	<!-- Populated dynamically. --> </p>
	</template>

	<!-- Load scripts. -->
	<script type="text/javascript" src="../../vendor/umbrella/3.3.0/umbrella-3.3.0.min.js"></script>
	<script type="text/javascript" src="../../vendor/umbrella/jquery-compat.js"></script>
	<script type="text/javascript">

		// The template element exposes a read-only property, "content", which is a
		// FRAGMENT that contains a non-rendered sub-tree of the Document Object Model
		// (DOM). In general, Fragments allow us to build-up sub-trees without causing
		// reflows of the rendered document.
		var fragment = u( "template" ).prop( "content" )
		// While we don't strictly need to clone the fragment in this demo (since we're
		// only using one copy), this is generally how templates are consumed. Let's
		// create a deep-clone of the template and wrap it in an Umbrella instance.
		var clone = u( fragment.cloneNode( true ) );

		// Now, let's try to change the text on the child paragraph both BEFORE and AFTER
		// appending the fragment to the body.
		// --
		// CAUTION: This will not work!!
		clone.find( "p" ).text( "Before appending to body." );
		clone.appendTo( document.body );
		clone.find( "p" ).text( "After appending to body." );

	</script>

</body>
</html>

As you can see, I'm wrapping the fragment in an Umbrella JS instance. Then, I'm using the .find() methods to try and locate the embedded paragraph tag. And, when we run this in the browser, we get the following output:

Document showing text that was applied prior to DOM node injection.

Notice that the rendered DOM only shows the "Before appending" text, not the "After appending" text. That's because the Umbrella JS wrapper for the fragment was emptied out the moment I appended the fragment to the body element. As such, the call to:

clone.find( "p" )

... resulted in an empty Umbrella JS collection, which happily turned the .text() call:

.text( "After appending to body." )

... into a silent no-op.

This only happens because I was re-finding the p tag via the fragment wrapper. If I had stored a reference to the p tag directly, this code would have worked as expected.

To be clear, none of this is a bug. Both Umbrella JS and the DocumentFragment are working just as they are documented. The only error here was in my mental model for how fragments work; and, how they change once injected into the rendered DOM. I'm only writing this up as a means to pound it into my head.

Want to use code from this post? Check out the license.

Reader Comments

Post A Comment — I'd Love To Hear From You!

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.