Caveat When Using Umbrella JS With Template Elements In JavaScript
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 usingNode
interface methods such asappendChild()
orinsertBefore()
. Doing this moves the fragment's nodes into the DOM, leaving behind an emptyDocumentFragment
.
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:
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
On a slightly related note, I just realized that you could mutate the contents of a
<template>
element the same way you can mutate any other DOM element:www.bennadel.com/blog/4619-html-templates-can-be-mutated-just-like-any-other-dom.htm
And, any changes you make to a template's
.content
will be reflected in any subsequent cloning of the template's document fragment.Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →