Can I filter by values set with jQuery.data

1.1k views Asked by At

I want to use a .filter function against a large number of elements where I'm checking for a data-* attribute, however they are set by the .data method.

The problem is that the jQuery selector always to use the original value. If I set a data value using $('whatever').data('key', newValue) and then try to use a selector $('[data-key=newValue]') then nothing is found.

Here's my test HTML:

<div id="test" data-dummy="foo"></div>
<div id="result"</div>

Then:

$('#test').data('dummy', 'bar');

$('#result').
    append('<div>Where dummy=foo: ' + $('[data-dummy="foo"]').length + '</div>').
    append('<div>Where dummy=bar: ' + $('[data-dummy="bar"]').length + '</div>');

Outputs:

Where dummy=foo :1

Where dummy=bar :0

It looks like, for the selector engine, only the original value is used in selectors.

Update - cheers to initial answers, this is actually on purpose. .data starts with data-* values, but sets its own copy. I don't want to replace every call to .data( with a call to .attr('data-' + .

To work around this I'm using .filter with a function, but as this filter will run for large numbers of elements I don't want to create a new jQuery object for each match. For this sort of circumstance jQuery provides some global functions.

Basically I should be able to use $.data(element, instead of $(element).data(, but this doesn't work either: $.data(element, returns undefined.

So my code ends up something like .filter(function(){return $.data(this, key) === value;})

However that appears to only be before a jQuery object is initialised:

var domEle = document.getElementById('test');
var globalBefore = $.data(domEle, 'dummy');
var first = $(domEle).data('dummy');
var globalAfter = $.data(domEle, 'dummy');

$('#result').
    append('<div>$.data before: ' + globalBefore + '</div>').
    append('<div>new $(): ' + first + '</div>').
    append('<div>$.data after: ' + globalAfter + '</div>');

Outputs:

$.data before: undefined

new $(): foo

$.data after: foo

Weird. Never mind, the question is can I work around it? Is there any way to initialise whatever's happening for a new $ object without creating one for every tested node?

JS Fiddle demo: http://jsfiddle.net/kU4th/

3

There are 3 answers

3
T.J. Crowder On BEST ANSWER

It's not a bug, it's just really confusingly-designed behavior.

The problem is that the jQuery selector seems to use the original value. If I set a data value using $('whatever').data('key', newValue) and then try to use a selector $('[data-key=newValue]') then nothing is found.

That's because data never sets data-* attributes, it just initializes itself from them. It's assymetrical (reads from them but doesn't write to them).

To actually update the attribute, use attr. E.g., instead of:

$('whatever').data('key', newValue);

you need

$('whatever').attr('data-key', newValue);

If you want to both set the data and the attribute:

$('whatever').data('key', newValue).attr('data-key', newValue);

data associates key/value pairs of data with elements using a jQuery-managed cache of objects. If you ask data for a key that doesn't already exist in the element's data cache, it looks to see if that key matches a data-* attribute and, if so, initializes the data cache with that value. At that point, there is no further connection between data and the data-* attribute.

The reason data doesn't write back to the attributes is probably at least partially down to the fact that you can store anything via data, whereas of course attribute values are always strings. So for instance, if you do:

$("whatever").data('key', {
    nice:   "complex",
    obj:    "here",
    answer: 42,
    when:   new Date()
});

...then later $("whatever").data('key') will give you back that object. That object cannot be stored in an attribute.

You're not, by far, the first person to be burned by this surprising design. :-)


Basically I should be able to use $.data(element, instead of $(element).data(, but this doesn't work either: $.data(element, returns undefined.

Right. The documentation for $.data tells you why:

Note: This is a low-level method; a more convenient .data() is also available.

Regarding HTML5 data-* attributes: This low-level method does NOT retrieve the data-* attributes unless the more convenient .data() method has already retrieved them.

You've said you don't want to replace calls to data with calls to attr, but I have to say that seems like the best situation, so you can actually use those data-* attributes in selectors.

Alternately, you can do this:

matchingElements.filter(function(){
    return ($.data(this, key) || this.getAttribute('data-' + key)) === value;
});

...which first looks at the data data and, if it gets back a falsey value (undefined, null, "", false, NaN, or 0), goes and gets the attribute from the element instead. If you want to limit that to just undefined:

matchingElements.filter(function(){
    var value = $.data(this, key);
    return typeof value === "undefined" ? this.getAttribute('data-' + key) : value;
});
0
UweB On

jquery's data method and the HTML5 data-* attributes are completely different animals.

See documentation:

Regarding HTML5 data-* attributes: This low-level method does NOT retrieve the data-* attributes unless the more convenient .data() method has already retrieved them.

What you set through

$(selector).data('key', value)

you retrieve through

var foo = $(selector).data('key')

If you want to use html data-* attributes, use

$(selector).attr('data-key', value)

and retrieve it with

var foo = $(selector).attr('data-key');
0
Guffa On

jQuery reads data attributes, but then the data is stored separately from the attributes. Setting data on an element doesn't affect the attributes.

If you want the attributes to be changed also, you need to use the attr method to set the value instead of the data method:

$('#test').attr('data-dummy', 'bar');

This will set both the data-dummy attribute on the element, and the value of data('dummy') for the element.