Storing custom data on elements

Let’s just come right out and say it: global variables suck. Except for the infrequent, truly global values, it’s hard to imagine a worse place to store information that you’ll need while defining and implementing the complex behavior of your pages. Not only do you run into scope issues, but they also don’t scale well when you have multiple operations occurring simultaneously (menus opening and closing, Ajax requests firing, animations executing, and so on).

The functional nature of JavaScript can help mitigate this through the use of closures (if you need a refresher on this topic, read the appendix), but closures aren’t appropriate for every situation.

Because your page behaviors are so element-focused, it makes sense to use the elements themselves as storage scopes. Again, the nature of JavaScript, with its ability to dynamically create custom properties on objects, can help you out here. But you must proceed with caution. Because DOM elements are represented by JavaScript object instances, they, like all other object instances, can be extended with custom properties of your own choosing. But dragons await!

These custom properties, so-called expandos, aren’t without risk. In particular, it’s easy to create circular references that can lead to serious memory leaks—for example, keeping a reference to an element you don’t need anymore. In traditional web applications, where the DOM is dropped frequently as new pages are loaded, memory leaks may not be as big an issue. But for authors of highly interactive web applications that employ lots of script on pages that may hang around for quite some time, memory leaks can be a huge problem.

jQuery comes to your rescue by providing a means to tack data onto any DOM element that you choose, in a controlled fashion, without relying on potentially problematic expandos. You can place any arbitrary JavaScript value, even arrays and objects, on DOM elements by using the cleverly named data() method. This is the syntax.

Method syntax: data
data(name, value)
Adds the passed value to the jQuery-managed data store for all elements in the set
Parameters
name(String) The name of the data to be stored
value(Any) The value to be stored except undefined
Returns
The jQuery collection

The data() method treats camel-case names the same way as dashed (or hyphenated) names. The following statement

$('.class').data('lastValue');

is the equivalent of

$('.class').data('last-value');

Besides, unlike the attr() method that stores values always as strings, data() is able to keep the value’s type. data() also tries to convert an attribute’s value to its native type when used as a getter. Let’s look at an example.

Let’s say you have this code in your page:

You’ll see "string" on the console because attr() has converted the number 10 into its string equivalent ("10"). On the other hand, if you write

you’ll see "number" on the console because data() keeps the value’s type as is.

Now imagine you have the following HTML code:

<input id="name" name="name" data-mandatory="true" />

You’ll obtain different results using attr() and data(), as follows:

The attr() method retrieves the value as a string, so by printing its type you obtain "string". On the other hand, data() is able to convert the value to a Boolean (the same applies to numbernull, and so on), so you see "boolean".

It’s worth noting that undefined isn’t recognized as a value but still returns a jQuery object. Therefore, a statement such as

$('#name').data('mandatory', undefined);

doesn’t modify the value of mandatory, but it returns the jQuery object that it was called on, which allows for method chaining.

Having the ability to add new data to an element is nice, but so far you’re limited to adding one item of data at a time, which isn’t very practical. Fortunately, jQuery has a variant of data() as a setter that accepts an object of key-value pairs.

Method syntax: data
data(object)
Adds the key-value pairs of the given object to the jQuery-managed data store for all elements in the set
Parameters
object(Object) An object of key-value pairs to be stored
Returns
The jQuery collection

We’ll continue our exploration of the data() method soon, but first we want to mention that jQuery also provides a utility function of the jQuery object that’s called and acts in the same way as the previously explained data() method.

jQuery.data() (or equivalently $.data()) is a low-level method because it acts on a DOM element instead of a jQuery object. This method accepts the same parameters of data() but introduces a new one (first in the list of parameters) where you can pass the DOM element on which you want to store the data.

To give you an idea of their differences, let’s say that you have an element having app as its ID and you want to store a given value using $.data() (the method that doesn’t work with a jQuery object). You can do this as shown here:

$.data(document.getElementById('app'), 'price', 10);

If using data() (the method that works with a jQuery object), you’d call it as follows:

$('#app').data('price', 10);

It should be no surprise that the data() method is also able to read data, not just write it. Here’s the syntax for retrieving data using the data() method.

Method syntax: data
data([name])
Retrieves any previously stored data or an HTML5 data-* attribute with the specified name on the first element of the set. If a name isn’t specified, the method returns an object containing all the previously stored data.
Parameters
name(String) The name of the data to be retrieved.
Returns
The retrieved data, or undefined if not found.

The behavior of the data() method as a getter is interesting and may lead to some confusion, so it deserves further discussion.

The getter form of this method helps in retrieving data stored in memory using its setter version. If the previously stored data isn’t found, the method searches for a data-* attribute of the HTML element with the same name given. Once data() retrieves a value from a data-* attribute, the method stores that value in the jQuery-managed data store (consider it as internal memory jQuery uses to keep track of all sorts of things). Therefore, any following call to the method will no longer retrieve the value from the attribute, even if you changed the latter using the attr() method, because it’s now stored in the jQuery memory. In case the attribute isn’t found, undefined is returned. Figure 4.3 shows the described flow to help you visualize this.

Figure 4.3. How data() searches for stored information. The getter form of this method helps you retrieve data stored using its setter version. If the required stored data isn’t found, the method searches for a data-* attribute of the HTML element with the same name given. If this attribute isn’t found, undefined is returned.

This process can be tricky, so we’ll look at some examples. Let’s say that you have the following element:

<input id="level1" type="text" value="I'm a text!" data-custom="foo" />

You want to retrieve the value of data-custom. You can do that using either the data() or the attr() method but with different parameters. Here’s how:

Both previous statements print the string "foo" on the console. But what happens if you use the data() method as shown here?

You changed the value of custom using the data() method and tried to retrieve it using both the getter version of data() and attr(). This time the result is different! Sounds crazy? You’re not finished yet.

As our last example, imagine that you have the previous element without any previously added data because you’ve just loaded the page. This time you want to prove that once the value of an attribute of an HTML element is retrieved for the first time using data(), the value managed by data() is completely independent from the attribute and thus any future call to attr(). To see this behavior in action, you’ll retrieve the value of the attribute using data(), then you’ll change the attribute using attr(), and finally you’ll invoke attr() and data() to highlight the difference. This description is implemented by the following code:

If you want to play with this example to better understand the concept, you can open the file lesson-4/getting.and.setting.data.html or access the relative JS Bin we’ve created.

The jQuery.data() method we introduced previously can also be used to retrieve the stored data. To do that, you need to pass the DOM element on which you want to operate as the first parameter and the name of the data to retrieve as the second. In this case, too, the name of the data is optional. If you omit it, jQuery retrieves all the data stored for the given DOM element. The jQuery.data() utility function can be used to retrieve data stored using the data() method and vice versa.

jQuery 3: Bug fixed

jQuery 3 fixes a bug of the data() method that occurred when dealing with attributes with digits in the name—for example, if you have the following element:

<div id="name" data-foo-42="test"></div>

If you’re using a version of jQuery prior to 3 and you write

console.log($('#name').data());

instead of an object containing the property foo-42 with a value of test, you obtain an empty object.

jQuery 3: Feature changed

jQuery 3 changes the behavior of the data() method to align it to the Dataset API specifications. In particular, the main change is that jQuery will transform all the properties’ key to be camel case. To understand this change, consider the following element:

<div id="name"></div>

If you’re using a version of jQuery prior to 3 and you write

$('#name').data({'my-property': 'hello'});
console.log($('#name').data());

you’ll obtain the following result on the console:

{my-property: "hello"}

In jQuery 3, you’ll obtain this result instead:

{myProperty: "hello"}

Note how in jQuery 3 the name of the property is in camel case while in jQuery 1.11 and 2.1 it contains a dash. In case you want to learn more about this change.

jQuery not only has methods to set and get data, but in the interests of proper memory management, it also provides the removeData() method as a way to dump any data that may no longer be necessary. This method allows you to remove values that were previously set using data(), but it doesn’t affect any HTML5 data-* attribute of the element (you have to use removeAttr() for this). The syntax of this method is shown here.

Method syntax: removeData
removeData([name])
Removes any previously stored data with the specified name on all elements of the jQuery object. The parameter can also be an array or a string of space-separated names to remove. If no arguments are given, all values are removed.
Parameters
name(String|Array) A string containing the name or names separated by a space of the data to be removed. If an array is provided, its elements are used to search the names of the data to remove.
Returns
The jQuery collection.

Based on what you’ve just learned, to remove all the data stored for a given element you can write the following:

$('#legal-age').removeData();

If you want to remove the foo and bar data, you can write

$('#legal-age').removeData(['foo', 'bar']);

or

$('#legal-age').removeData('foo bar');

Note that it’s not necessary to remove data manually when removing an element from the DOM with jQuery methods; the library will smartly handle that for you.

As for the data() method, jQuery provides a utility function equivalent to removeData(). This utility is called jQuery.removeData() (or $.removeData(), using the jQuery alias), and it accepts as its first parameter the DOM element on which you want to remove the data, and optionally the name of the data to remove as its second parameter. So, if you want to employ $.removeData() to remove all the data stored on the element having legal-age as its ID, you’ll have to write

$.removeData(document.getElementById('legal-age'));

In addition to the jQuery.removeData() method, jQuery has another utility function that deals with data stored on an element. This method, called jQuery.hasData(), allows you to test whether a DOM element has any jQuery data associated with it. Its syntax is as follows.

Method syntax: jQuery.hasData
jQuery.hasData(element)
Determines whether an element has any associated data
Parameters
element(Element) A DOM element to be checked for data
Returns
true if there’s any data associated with the element; false otherwise

To see how you can use it, you’ll test for the presence of any data on an element having legal-age as its ID, store some data, and then run the test again. Thus, you expect the first test to return false and the second to return true. Let’s see jQuery.hasData() in action:

We’ll exploit the capability to tack data onto DOM elements to our advantage in many of the examples in the upcoming lessons. But if you’ve run into the usual headaches that global variables can cause, it’s easy to see how storing data in context within the element hierarchy opens up a whole new world of possibilities. In essence, the DOM tree has become a complete “namespace” hierarchy for you to employ; you’re no longer limited to a single global space.

We mentioned the className property much earlier in this section as an example of a case where markup attribute names differ from property names, but truth be told, class names are a bit special in other respects as well and are handled as such by jQuery. The next lesson describes a better way to deal with class names than by directly accessing the className property or using the attr() method.


by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *