128

Forgive me for not being more specific on this. I have such a strange bug. After the doc loads, I loop some elements that originally have data-itemname="", and I set those values using .attr("data-itemname", "someValue").

Issue: When I later loop thru those elements, if I do elem.data().itemname, I get "", but if I do elem.attr("data-itemname"), I get "someValue". It's like jQuery's .data() getter only gets elements that are set initially to contain some value, but if they are originally empty, and later set, then .data() doesn't get the value later on.

I've been trying to recreate this bug but have not been able to.

Edit

I have recreated the bug! http://jsbin.com/ihuhep/edit#javascript,html,live

Also, snippets from above link...

JS:

var theaters = [
    { name: "theater A", theaterId: 5 },
    { name: "theater B", theaterId: 17 }
];

$("#theaters").html(
    $("#theaterTmpl").render(theaters)
);

// DOES NOT WORK - .data("name", "val") does NOT set the val
var theaterA = $("[data-theaterid='5']");
theaterA.find(".someLink").data("tofilllater", "theater5link"); // this does NOT set data-tofilllater
$(".someLink[data-tofilllater='theater5link']").html("changed link text"); // never gets changed

// WORKS - .attr("data-name", "val") DOES set val
var theaterB = $("[data-theaterid='17']");
theaterB.find(".someLink").attr("data-tofilllater", "theater17link"); // this does set data-tofilllater
$(".someLink[data-tofilllater='theater17link']").html("changed link text");

HTML:

<body>
    <div id="theaters"></div>
</body>

<script id="theaterTmpl" type="text/x-jquery-tmpl">
    <div class="theater" data-theaterid="{{=theaterId}}">
        <h2>{{=name}}</h2>
        <a href="#" class="someLink" data-tofilllater="">need to change this text</a>
    </div>
</script>
isherwood
  • 58,414
  • 16
  • 114
  • 157
Ian Davis
  • 19,091
  • 30
  • 85
  • 133

7 Answers7

240

I ran into a similar "bug" a few days ago when working with .data() and .attr('data-name') for HTML5 data attributes.

The behavior you're describing is not a bug, but is by design.

The .data() call is special - not only does it retrieve HTML5 data attributes it also attempts to evaluate/parse the attributes. So with an attribute like data-myjson='{"hello":"world"}' when retrieved via .data() will return an Object while retrieval via .attr() will return a string. See jsfiddle example.

Since .data() does extra processing jQuery stores the results of attribute evaluation in $.cache - after all, once a data attribute has been evaluated it would be wasteful to re-evaluate on every .data() call - especially since data variables can contain complex JSON strings.

I said all that to say the following: After retrieving an attribute via .data() any changes made by .attr('data-myvar', '') will not be seen by subsequent .data() calls. Test this out on jsfiddle.

To avoid this problem don't intermix .data and .attr() calls. Use one or the other.

leepowers
  • 37,828
  • 23
  • 98
  • 129
  • marking this as answer. please see this test for all possible scenarios with .data() vs. .attr(), which includes getting & setting using each, and outputting the length of the selectors after they've been set, http://jsbin.com/acegef/edit#javascript,html,live – Ian Davis Jan 03 '12 at 20:34
  • 9
    At least this explains the apparently non-intuitive behavior... though this could cause some smaller headaches when one's using 3rd party libraries, some of them using only data(), and others changing the actual attribute. – Haroldo_OK Oct 11 '13 at 12:55
  • 1
    @leepowers, "After retrieving an attribute via .data() any changes made by .attr('data-myvar', '') will not be seen by subsequent .data() calls", if thats really beacuse of the cache($.cache), then it seems no good! instead of relying blindly on cache, subsequent .data() calls could do some basic checks() like whether the value empty now or string length changed (matching the cache with current value) – MinhajulAnwar Nov 06 '15 at 02:52
  • 1
    It should be noted that trying to set data('id', val) also fails if the value was not retrieved earlier... great design of the data() function by Jquery. – andreszs Apr 23 '17 at 16:02
  • This "feature" has caught me multiple times... Due to the great explanation, it never will again! +! – J. Allan Mar 08 '18 at 22:10
  • 3
    So, it's the data() cache that's holding me for hours. No wonder why no matter how much I change its value, it still returns the same value. Thanks for this. – Lynnell Neri May 30 '18 at 13:00
  • Thank you for the great explanation, which shed light on my problem as well! However, this design is problematic when one wants to use a selector such as `$('[data-my-id="Something"]')`. Since `data` does not change the dom, one needs to do both use data and set the attr as well, as suggested by Elmar Höfinghoff – Jacques Oct 20 '18 at 14:46
26

This is the result of a misunderstanding: data is NOT an accessor for data-* attributes. It's both more and less than that.

data is an accessor for jQuery's data cache on the element. That cache is initialized from data-* attributes if there are any present, but data never writes to the attributes, nor does changing the attribute change the data cache after initialization:

const div = $("[data-example]");
console.log('div.data("example"):', div.data("example"));
console.log('div.attr("data-example"):', div.attr("data-example"));
console.log('Using div.data("example", "updated")');
div.data("example", "updated");
console.log('div.data("example"):', div.data("example"));
console.log('div.attr("data-example"):', div.attr("data-example"));
<div data-example="initial value"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

data also massages what it finds in various ways, guessing at data types, making data("answer") on an element with data-answer="42" a number, not a string, or even parsing things as JSON if they look like JSON:

console.log(typeof $("[data-answer]").data("answer"));
console.log(typeof $("[data-json]").data("json"));
console.log(typeof $("[data-str]").data("str"));
<div data-answer="42"></div>
<div data-json='{"answer":42}'></div>
<div data-str="example"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

If you want to use the attributes (both reading and setting them), use attr, not data. attr is an accessor for attributes.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
21

.attr("data-itemname", "someValue") modifies the DOM.

.data("itemname", "someValue") modifies the jQuery cache.

To get this working in following Javascript and in addition in CSS you have to set both.

theaterA.find(".someLink").attr("data-itemname", "someValue");
theaterA.find(".someLink").data("itemname", "someValue");
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
6

That's because the attribute's name is data-itemname. You cannot use - in the shorthand obj.attribute notation (obj.data-itemname would be intepreted as "obj.data minus itemname").

Marc B
  • 356,200
  • 43
  • 426
  • 500
4

Why don't you just use .data() everywhere?

You can also declare default values inline on the HTML, which is fine too.

<span data-code="pony">text</span>

and

$("span").data("code") == "pony" // true

if you want to change it you just do

$("span").data("code", "not-a-pony");

and to remove it altogether you could invoke

$("span").removeData("code");

you should really try and avoid using .attr("data-*"), I don't know why you'd want to do so anyways.

bevacqua
  • 47,502
  • 56
  • 171
  • 285
  • s/should/must, using `.attr('data-*', ...)` won't make the data visible to `.data()` – ThiefMaster Jan 03 '12 at 02:27
  • But isn't doing it with attr() the way you'd have to do it _without_ jQuery? (with getAttribute tho) – powerbuoy Jan 03 '12 at 02:30
  • If you don't have jQuery you use `getAttribute()` and `setAttribute()` - so both methods will access the actual attributes and it will work again. Or you'd just use the `dataSet` property modern browsers provide. – ThiefMaster Jan 03 '12 at 02:31
  • If you have jQuery you might as well use it as it's supposed to be used, otherwise what's the point of having it in the first place? – bevacqua Jan 03 '12 at 02:31
  • @Nico - The `.data()` function attempts to cast the data value as a Javascript variable while `.attr()` always returns the data value as a string. For instance `.data()` will attempt to decode a JSON string found in a data value and return the resultant object, while `.attr()` will return the JSON string only. – leepowers Jan 03 '12 at 04:56
  • @powers1 that's what I meant when I said "use it as it's supposed to be" – bevacqua Jan 03 '12 at 10:58
  • if you do `.data("code", 555)` and later try to select those elements with `$("[data-code='555']")`, the length will be zero on that selector. This is illustrated in my jsbin link in original (edited) question. thanks. – Ian Davis Jan 03 '12 at 14:29
  • 1
    Don't select elements like that. It's horribly inefficient since it will have to iterate over *every single* element in the document. Use a `class` instead since browsers have native functions to get elements with a certain class. – ThiefMaster Jan 03 '12 at 14:54
  • @ThiefMaster - thanks for the advice! so, something like $(".someLink[data-code='555']") ? where there are several ? – Ian Davis Jan 03 '12 at 16:27
  • No, `$('.someLink-555')` and `.addClass('someLink-555')` – ThiefMaster Jan 03 '12 at 17:06
  • 1
    but, "555" is data, so I should be using the data() logic. putting that data in the class name is mixing data with presentation. different way of doing it I suppose. – Ian Davis Jan 03 '12 at 17:21
  • @Ian what you said is correct, you should be doing `$(".someLink")` and either iterate to find the data you want, or add a data filter that matches your needs. – bevacqua Jan 03 '12 at 17:40
1

You must set the data using .data('itemname', 'someValue');. Using .attr() to set data attributes won't work: http://jsfiddle.net/ThiefMaster/YHsKx/

However, you can provide inline values by using e.g. <div data-key="value"> in the markup.

ThiefMaster
  • 310,957
  • 84
  • 592
  • 636
  • in the jsfiddle, the get works after you do both sets. so, doing attr("data-itemname", "value") DOES work. I'm using Chrome. – Ian Davis Jan 03 '12 at 02:53
  • @Ian uh, no. the `.data()` call sets the attribute, while the `.attr()` call does nothing. – bevacqua Jan 03 '12 at 02:59
  • @IanDavis: Click "set attr" and "get" will give you an empty object. Only "set data" works. – ThiefMaster Jan 03 '12 at 11:59
  • @Nico - this is exactly what I did: (1) click "set attr" button, then (2) click "get" button. this is what happened: it alerted me, "{"test":"meow"}". so, the .attr() set the attribute. otherwise, the alert would have been empty. If you follow those exact steps, do you get different results? thanks. – Ian Davis Jan 03 '12 at 14:36
  • @ThiefMaster - please see my previous comment. am I doing something out of order? I'm using Chrome. thanks guys for your help. – Ian Davis Jan 03 '12 at 14:37
  • I only tried it in Firefox. Maybe the behaviour differs. Anyway, if you simply do not use `.attr()` it will work. – ThiefMaster Jan 03 '12 at 14:53
  • 1
    @ThiefMaster - in theaterA in my provided code, I'm not using `.attr()` at all, only `.data()`, and, the length of the selector, `$(".someLink[data-tofilllater='theater5link']")`, is zero. so it's like I have to use `.attr()` :/ – Ian Davis Jan 03 '12 at 16:30
  • @IanDavis Did you ever find out the exact reason why Chrome does not respect setting data via jQuery? – Alexander Dixon Jun 12 '17 at 15:26
0

I can see that this has brought up some division on how to approach the data attribute setting.

I too run into this problem and I found that the issue seemed to be simply the data attribute name formatting.

In my experience, you should avoid using hyphens in the data variable (the variable name that comes after "data-").

This didn't work for me:

[Markup]

<div class="list" id="myElement1" data-item-order="some-value"> .... </div>

[jQuery]

jQuery("#myElement1").data("item-order", "my-new-value");

But the following worked just fine! :) :

(I use an underscore instead of a hyphen when required)

[Markup]

<div class="list" id="myElement1" data-item_order="some-value"> .... </div>

[jQuery]

jQuery("#myElement1").data("item_order", "my-new-value");

I hope it helps. Cheers everyone!

mmmdearte
  • 101
  • 2