jQuery 3 - Working With Data
Written by Ian Elliot   
Monday, 28 November 2016
Article Index
jQuery 3 - Working With Data
Finding and Using Data
Utility Functions

Data On Objects

There is a little known extension of the DataSet API implemented by jQuery. You can use the same functions that you use to work with data on DOM elements to work with standard JavaScript objects. If you wrap a JavaScript object as a jQuery object than you can use data() to set key value pairs.

The data is stored in the original object in a new property that is added called jQuery{randomnumber}. No key name transformations are made and no tags are involved - this is pure JavaScript. This is also the mechanism that jQuery uses to implement data on DOM elements so bypassing the dataset property which holds the values set by the .data-* attributes.

For example:

$(myObj).data("mykey","mydata");
console.log( $(myObj).data("mykey"));

will display mydata on the console. 

Notice that removeData doesn't work on objects.

You might be wondering why you would add data to objects in this way after all you can simply add data as standard properties?

In earlier versions of jQuery it was possible to make use of two custom events associated with any changed that occurred to the data. This however has been removed from jQuery 3 and no longer works. It is fairly easy to create a new custom event however but this isn't as compelling a reason to use jQuery data rather than properties in JavaScript objects.

Finding Data

Most applications of jQuery data or the Dataset API require you to find and process elements that have data set on them. This is fairly easy using the attribute selectors. The only complication is that you need to keep in mind that you have to use the dashed form of the name to find the DOM elements and the camel case form in the JavaScript.

For example to extract all DOM objects that have an attribute data-mydata-x you would use:

alert($("[data-mydata-x]");

and to get the value of the data you would use:

alert($("[data-mydata-x]").data("mydataX"));

See the section on Attribute selectors in chapter 2.

You can, of course find only the DOM objects that have a data- attribute set to a particular value or that contains a substring. For example

alert($("[data-mydata-x*='my']");

selects only elements with a data-mydata-x attribute with a value that contains the substring my.

Using attribute selectors you can pick out any DOM objects with a given attribute name and a value that is specified, prefixed, contains, ends or contains a specific word. This is flexible, but often you need to get all the elements that have a data-* attribute of any sort. The problem is jQuery doesn't provide any way to select elements with attribute names that match a pattern. It is fairly easy to create a filter function that will filter a results list to only include elements that have data.

There is a test function hasData that returns true if an element has data and false otherwise. This can be used in a filter function to reduce a result array to just the elements that have data: 

 var has= $("button").filter(function(){
                      return $.hasData(this);
          });

This finds all button objects and then filters out all of those that don't have data. 

If you try it out you will find that it only filters elements that you have used .data() to actually set data. That is it doesn't filter out elements that have data-* attributes. The hasData function is testing to see if jQuery has stored data not that there is a data-* attribute on the original tag. Notice that jQuery uses its data storage mechanism for internal tasks such as storing information about event handlers that are attached to an element. This means that you might get back elements that you didn't expect to be storing data from the filter function. 

There are many possible ways ot testing to see if an element has any data-* but the simplest is to check to see if the DOM object has a non-empty dataset property:

var has= $("button").filter(function(){
 return $.isEmptyObject($(this).prop("dataset"));
 });

This looks complicated but all that happens is that first we find all button elements and filter the results. The filter function simply finds the dataset property, all DOM objects have a dataset property, and then we use the utility function isEmptyObject to check if there is any data stored in the property. This reduces the list to only those elements that have data defined using data-* tags. Notice that this does not select elements that have had data set using nothing but the .data function as this does not add entries to the dataset property. 

Using Data

Once you understand how the Dataset API works you can immediately invent uses for it. In general you should use it to add data to tags that determines the way that JavaScript will process the DOM object.

Many JavaScript frameworks make use of data to implement a range of features that go beyond basic HTML without becoming non-standard. For example the dojo toolkit, jQuery mobile and WinJS all makes used of data-* to set control types and other options to create UI elements that don't exist in HTML. 

For example the WinJS library allows you to use a range of standard Windows controls within a JavaScript program using nothing but standard HTML. One of the examples given in the documentation illustrates this perfectly:

<div id="ratingControlHost"
     data-win-control="WinJS.UI.Rating"
     data-win-options="{maxRating: 10}"> </div>

This, after processing by the WinJS library produces a 10 star rating display:

stars

 

You can see that the win-control data specifies the type of control to be displayed and the win-options is an object with key value pairs which control the look of the control - a ten star rating in this case. 

Not only should be able to understand this you should also be able to implement this using what you know about jQuery. Let's see how.

First we need the HTML

<div id="ratingControlHost"
 data-win-control="WinJS.UI.Rating"
 data-win-options="{maxRating: 10,rating: 4}">
</div>

In this case we have also an inital rating set in the win-options data. To process this we just need to find any markup with an attribute data-win-control:

var control = $("[data-win-control]");

If we assume that there is only one this simplifies things and you need to remember that control is a jQuery object. Now we need to get the winControl data and test it:

var type = control.data("winControl");
if (type === "WinJS.UI.Rating") {

In a real application we would probably have many different types of control but we only have one so we can implement it in the if statement. Getting the options turns out to be more difficult than you might have expected. 

var options = control.data("winOptions");

That was easy but options is a string

{maxRating: 10,rating: 4}

and we need to access the various propertys. We can't convert this to an object using JSON.parse because this isn't valid JSON - there are no double quotes around every item. We could process it using nothnig but string handling but in this case the simplest solution is to use eval. Most JavaScript programmers have been taught that eval is evil - it isn't but you do have to be careful in its use. In this case the data is coming from a tag in a web page we have just created and while you can invent ways that this could be modified by a attacker it is difficult to see how it could be used to advantage - the attacker can run any JavaScript in the page that they want to anyway.

With this in mind you might want to implement this using string handling but in this case the simplest solution is:

options = eval("(" + options + ")");

Now options contains an object with the properties and values specified by the string. Getting the values is easy:

var maxRating = options.maxRating;
var rating = options.rating;

All that remains is to generate a display of rating black stars and maxRating-rating white stars. If you want to do this in an impressive way you need one or two star graphics. For simplicity we will use Unicode characters 9733 and 9734 - a black and white star symbol respectively. To generate a string with the correct number of stars is a matter of a for loop:

for (i = 0; i < maxRating; i++) {
 if (i < rating) {
  stars += String.fromCharCode(9733);
 } else {
  stars += String.fromCharCode(9734);
}
}

Finally we add the string to the div's DOM object:

control.append(stars);

The result isn't as impressive as the WinJS control, but you can see that with some CSS and some images it could be just as good:

stars2

The complete program is:

var control = $("[data-win-control]");
var type = control.data("winControl");
var test=$.isArray(control);
if (type === "WinJS.UI.Rating") {
 var options = control.data("winOptions");
 options = eval("(" + options + ")");
 var maxRating = options.maxRating;
 var rating = options.rating;
 var stars = "";
 for (i = 0; i < maxRating; i++) {
  if (i < rating) {
   stars += String.fromCharCode(9733);
  } else {
   stars += String.fromCharCode(9734);
  }
 }
control.append(stars); }

You can see that there is scope for writing this in a more jQuery idiomatic style but this form shows the steps more clearly.

You can also use data-* attributes to set up data binding. All that happens in this case is that you specify the name of a variable to be used to say set the number of stars in the rating. If the user clicks on the stars to change the rating then an event is used to update the bound variable. If the binding is two way then you also have to write some code to refresh the display if the variable is assigned to - simply use a get/set function that updates the display. 



Last Updated ( Thursday, 08 December 2016 )