Cloning Nodes - Building Our UI - Developing Web Components (2015)

Developing Web Components (2015)

Part II. Building Our UI

THE DIALOG WIDGET

In Part II, we will continue constructing the dialog widget introduced in Part I. If you skipped the first part of the book and would like to learn about the design and implementation details of the dialog widget, please refer back to Chapter 2. Subsequent chapters in Part I contain sections that apply the concepts they cover to the dialog widget as well. These sections are located toward the ends of the chapters and can be referenced as needed for understanding code in the dialog widget not introduced in Part II.

Part II introduces concepts and patterns that are typically abstracted away from day-to-day frontend development by libraries such as Dojo, jQuery UI, Kendo UI, and Twitter Bootstrap. These concepts and patterns are used to implement UI widgets that you probably work with on a daily basis as a frontend engineer. After reading this part of the book, you will have a solid understanding of how elements are positioned and how to manipulate element positions using JavaScript.

NOTE

If you skipped Chapter 2, you might find yourself asking, “Why are we building a widget? I thought this was a web components book!” That’s a valid question. The answer is that this book is more than just a rehashing of the Web Components specification. It is designed to provide a better understanding of the DOM by exploring the details underlying today’s widget interfaces. Knowledge of these details is required to build moderately complex web components, and what better way to understand these details than through a concrete example?

Chapter 5. Cloning Nodes

Jason Strimpel

Frequently when you see a drag-and-drop interface on the Web, nodes are being cloned—that is, copies of the original nodes are being made (using the cloneNode method, described in the next section). This is done because it is more efficient to clone the node that is being dragged than it would be to detach it from its original location in the DOM, reattach it, and then move it. This is especially true in the case where a user tries to drop the node outside of the defined drop target—the cloned node is deleted and the original node is then shown again, without having to be detached and reattached. If the cloned node is successfully dropped into the target node, then it remains, and the original node is deleted. Cloning can also be useful in the case where you do not want the original node to be removed or hidden until the cloned node is dropped into a target.

Another use case in which cloning is useful is for copying nodes. For instance, an interface may need to provide the functionality for classifying items by one or more characteristics. This functionality could be accomplished by creating an interface that allows a user to drop items into different drop targets, which could be easily accomplished by cloning nodes.

A third use case is that you want to perform some calculations based on a node’s dimensions. For instance, if the node contains text of an unknown length that needs to be truncated in a unique fashion—e.g., the beginning and end of the text need to remain, but an undetermined amount needs to be trimmed from the middle—then the containing node can be cloned and the necessary measurements can be done in a rendering layer below the default rendering layer, out of view. Once the string has been truncated accordingly, it can be inserted into the original node, and the clone can then be deleted.

TIP

Cloning nodes incurs overhead and is not always the best approach. Cases where a simple click event or form value change moves a node to another location in the DOM are not good cloning candidates, because these types of actions are not subject to the error that can occur when a human is manually moving nodes.

Using the cloneNode Method

All browsers natively support cloning nodes via a node’s cloneNode method. The method accepts an optional argument for creating a deep clone, which clones all descendant nodes:

var el = document.querySelector('.some-class');

// shallow clone

var elShallowClone = el.cloneNode(false);

// deep clone

var elDeepClone = el.cloneNode(true);

WARNING

The default value for cloneNode’s deep argument has changed between browser versions as the specification has changed. The current default value is false. To ensure backward compatibility, always pass the deep argument.

When a node is cloned, all of its attributes and values are copied to the clone. This includes inline listeners, but it does not include any event listeners bound using addEventListener or those assigned to element properties:

<!-- inline event handler -->

<!-- this will be bound to the cloned node -->

<div onclick="someFunction();">Click Me</dv>

var el = document.querySelector('.some-class');

// addListener; this will not be bound to the cloned node

el.addEventListener('click', function (e) {

// someone clicked me

}, false);

// bound using element property; this will not be bound to the cloned node

el.onclick = function (e) {

// someone clicked me again

};

cloneNode does not attach the returned node to the document. The node can be attached to the document using appendChild or a similar method for adding a node to the document:

var elToClone = document.querySelector('.clone-me-please');

var elToAppend = elToClone.cloneNode(true); // ***not part of the DOM***

// add cloned node to the DOM

document.body.appendChild.appendChild(elCloneToAppend);

WARNING

cloneNode copies all attributes and properties, so be careful not to create duplicate element IDs when cloning nodes. Duplicate IDs are technically not allowed because they are supposed to be unique identifiers, but in practice browsers do allow them. Having duplicate IDs can cause problems when code is expecting only a single element to be returned when querying for an element by ID.

Using jQuery.clone

The native cloneNode method works well for cloning nodes and their attributes, but it does not do a good job of cloning events bound to the element being cloned. Fortunately, jQuery has a method, $.clone, for ensuring that all events bound by jQuery can be copied to the new element if this is the desired behavior. This means that any events bound by $.on, $.bind, $.click, etc. are copied if the withDataAndEvents or deepWithDataAndEvents argument is set to true.

TIP

$.clone always does a deep clone (i.e., clones all descendant nodes of the nodes matched by the query selector passed to $), so use caution when cloning. Otherwise, you could end up copying very large branches of the DOM tree, which could be a very costly operation.

The $.clone method is used as follows:

var $el = $('.clone-me-please');

// clone without data and events

var $clone1 = $el.clone(false);

// clone with data and events for $el and all children

var $clone2 = $el.clone(true);

// deep clone with data and events for only $el

var $clone3 = $el.clone(true, false);

jQuery is able to copy the events because it keeps an internal cache of all event handlers it binds. This is also what enables it to automatically remove event handlers when elements are explicitly removed ($.remove) or implicitly removed ($.html). This is done to help prevent any references to elements that have been removed from the DOM from being retained, which helps reduce memory leaks.

WARNING

$.clone does not copy event handlers bound to an element outside of jQuery unless they are inline event handlers (e.g., <div onclick="someFunction();">).

In addition to copying all event handlers bound by jQuery, jQuery.clone also copies all data that was related to an element via $.data.

WARNING

Arrays and objects are copied by reference, not by value, so all cloned nodes’ data as well as the original node’s data will point to the same arrays and objects. A shallow copy of an array or object can be done after the node is cloned, as seen in the following code examples.

You use the $.data and $.clone methods as follows:

// get a reference to a jQuery selection and set data

// on the element

var $el = $('.some-el');

$el.data({

arr: [1,2,3],

obj: { foo: 'bar' }

});

// get references to the data from the elements

var arr = $el.data('arr');

var obj = $el.data('obj');

// jQuery provides a utility method for copying arrays and objects

var arrCopy = $.extend([], arr);

var objCopy = $.extend({}, obj);

// clone $el and set copies

$clone = $el.clone(true);

$clone.data({

arr: arrCopy,

obj: objCopy

});

// if someone ever asks you how to copy an array or object in an interview...

var arrCopy = arr.slice(0);

var objCopy = {};

for (var k in obj) {

objCopy[k] = obj[k];

}

// then say whoomp, there it is!

// if they are past a certain age they will appreciate this reference

// and your poor sense of humor :)

Continuation of the Dialog Widget

Oftentimes, a widget gets a reference to a DOM element. This element is then used as the container for the widget. In other cases the element is cloned because the original element should not be modified. The dialog will be treated as the latter case because its containing element will be appended to the <body>. This makes managing the z-index much easier, because it will only be competing with other elements that are children of the <body>. Additionally, the dialog widget should not make any assumptions as to the consumer’s alternate intended uses of that element. It could be used for other purposes than just the dialog widget, and if it were used directly any modifications made to the element by the dialog widget could inadvertently impact other areas of the web application. For these reasons, the dialog widget will do a deep clone of the element passed during construction using $.clone if options.clone is true (this option enables developers to decide whether or not to clone, providing them with a convenient API for avoiding the aforementioned issues):

function Dialog (options) {

// optionally clone dialog $el

options.$el = options.clone ? $(options.$el).clone() :

$(options.$el);

// append to body

if (options.appendToEl) {

$(options.appendToEl).append(options.$el);

}

Voltron.call(this, options);

return this;

}

Summary

In this chapter we introduced the concept of cloning nodes and presented some common use cases. We then covered the native method for cloning nodes, cloneNode, and the jQuery method, jQuery.clone. We discussed the enhanced event handler copying provided by thejQuery.clone method, then took this knowledge and applied it to the dialog widget as an illustration of how to handle cases in which the node selected to create a widget should be copied and not used to directly to create the widget instance.

You now have the proper background information to clone nodes effectively in any situation, and a working example. For more information on cloning nodes please refer to the MDN cloneNode documentation and the jQuery.clone documentation.