jquery data selector
Asked Answered
M

9

183

I need to select elements based on values stored in an element's .data() object. At a minimum, I'd like to select top-level data properties using selectors, perhaps like this:

$('a').data("category","music");
$('a:data(category=music)');

Or perhaps the selector would be in regular attribute selector format:

$('a[category=music]');

Or in attribute format, but with a specifier to indicate it is in .data():

$('a[:category=music]');

I've found James Padolsey's implementation to look simple, yet good. The selector formats above mirror methods shown on that page. There is also this Sizzle patch.

For some reason, I recall reading a while back that jQuery 1.4 would include support for selectors on values in the jquery .data() object. However, now that I'm looking for it, I can't find it. Maybe it was just a feature request that I saw. Is there support for this and I'm just not seeing it?

Ideally, I'd like to support sub-properties in data() using dot notation. Like this:

$('a').data("user",{name: {first:"Tom",last:"Smith"},username: "tomsmith"});
$('a[:user.name.first=Tom]');

I also would like to support multiple data selectors, where only elements with ALL specified data selectors are found. The regular jquery multiple selector does an OR operation. For instance, $('a.big, a.small') selects a tags with either class big or small). I'm looking for an AND, perhaps like this:

$('a').data("artist",{id: 3281, name: "Madonna"});
$('a').data("category","music");
$('a[:category=music && :artist.name=Madonna]');

Lastly, it would be great if comparison operators and regex features were available on data selectors. So $(a[:artist.id>5000]) would be possible. I realize I could probably do much of this using filter(), but it would be nice to have a simple selector format.

What solutions are available to do this? Is Jame's Padolsey's the best solution at this time? My concern is primarily in regards to performance, but also in the extra features like sub-property dot-notation and multiple data selectors. Are there other implementations that support these things or are better in some way?

Mcclure answered 23/5, 2010 at 11:8 Comment(1)
Use the jquery ui core api.jqueryui.com/category/ui-core It has a :data() selectorFielding
A
100

I've created a new data selector that should enable you to do nested querying and AND conditions. Usage:

$('a:data(category==music,artist.name==Madonna)');

The pattern is:

:data( {namespace} [{operator} {check}]  )

"operator" and "check" are optional. So, if you only have :data(a.b.c) it will simply check for the truthiness of a.b.c.

You can see the available operators in the code below. Amongst them is ~= which allows regex testing:

$('a:data(category~=^mus..$,artist.name~=^M.+a$)');

I've tested it with a few variations and it seems to work quite well. I'll probably add this as a Github repo soon (with a full test suite), so keep a look out!

The code:

(function(){

    var matcher = /\s*(?:((?:(?:\\\.|[^.,])+\.?)+)\s*([!~><=]=|[><])\s*("|')?((?:\\\3|.)*?)\3|(.+?))\s*(?:,|$)/g;

    function resolve(element, data) {

        data = data.match(/(?:\\\.|[^.])+(?=\.|$)/g);

        var cur = jQuery.data(element)[data.shift()];

        while (cur && data[0]) {
            cur = cur[data.shift()];
        }

        return cur || undefined;

    }

    jQuery.expr[':'].data = function(el, i, match) {

        matcher.lastIndex = 0;

        var expr = match[3],
            m,
            check, val,
            allMatch = null,
            foundMatch = false;

        while (m = matcher.exec(expr)) {

            check = m[4];
            val = resolve(el, m[1] || m[5]);

            switch (m[2]) {
                case '==': foundMatch = val == check; break;
                case '!=': foundMatch = val != check; break;
                case '<=': foundMatch = val <= check; break;
                case '>=': foundMatch = val >= check; break;
                case '~=': foundMatch = RegExp(check).test(val); break;
                case '>': foundMatch = val > check; break;
                case '<': foundMatch = val < check; break;
                default: if (m[5]) foundMatch = !!val;
            }

            allMatch = allMatch === null ? foundMatch : allMatch && foundMatch;

        }

        return allMatch;

    };

}());
Arronarrondissement answered 24/5, 2010 at 9:25 Comment(13)
@J-P: Very sweet, I knew I could count on you! Headed to bed now, but I'll try it out tomorrow. Is it possible to do OR operations as well? I don't need it, just curious.Mcclure
@J-P: Also, I'm curious about your take on the other data selector solutions, pros/cons for yours vs. theirs. For instance, this plugin: plugins.jquery.com/project/dataSelector.Mcclure
It is possible to do OR operations, but it might be best to simply have two selectors, $("a:data(condition),a:data(orCondition)") ... it'll have the same effect. The more features you add, the slower it'll be. If the logic is complex, then use $(foo).filter(function(){...}).Arronarrondissement
The data selector you linked to seems okay... the implementation seems a bit lacking though.Arronarrondissement
@J-P: I think I found a bug in the AND operation. See the last alert() in this code: jsbin.com/ixadi3/2/editMcclure
Sorry about that. The return statement should have been return allMatch, not return foundMatch. Should be working now.Arronarrondissement
@J-P: Did you ever turn this into a github project? I'm running a test using jQuery 1.5.1 using the selector $("input:data(test>400)") on an input with the html5 attribute data-test="500" but the selector is returning nothing.Culch
Will $('a:data(category==music)') select the following anchor <a href="#" data-category="music">my link</a>?Standpipe
@JamesSouth That's because the selector in this answer uses the low level jQuery.data, which does not get the data defined in the HTML5 attributes. If you want that functionality, you can change jQuery.data to $('selector').data, but that's a trade off for speed.Slavism
I'm trying your code, but I can't make it works with this kind of field: data-my-value I tried with $('div:data(myValue==5213)') or $('div:data(my-value==5213)'), but both doesn't seems to worksFeticide
chrome throws unsupported pseudo: data error whenever I use :data selectorConcern
@999 This is great answer, thank you very much. What can you say about perfomance? Is it much slower then using data selector $('.class[data-smth="1"]) or the speed difference is so small, that I should don't take it into account?Adele
@Arronarrondissement I love your solution here and have been using it. I'm running into some performance issues. I have a question open (https://mcmap.net/q/137605/-performance-of-regex-within-jquery-data-selector-dependance-on-certain-string-length/1214731) if you're interested in following up.Aristate
B
175

At the moment I'm selecting like this:

$('a[data-attribute=true]')

Which seems to work just fine, but it would be nice if jQuery was able to select by that attribute without the 'data-' prefix.

I haven't tested this with data added to elements via jQuery dynamically, so that could be the downfall of this method.

Beaming answered 6/7, 2011 at 10:51 Comment(7)
This is exactly what I was looking for. SweetKalamazoo
This doesn't actually work if you attach/modify data via JS through the .data() function, the attribute selector only checks the DOM, JS stores .data() in memoryZachery
Could you please tell how do you access by attribute if you attach via .data()?Gasser
I'd suggest that the other answers provided in this thread are a better option for anything other than a very simple use case.Beaming
Dmitri's solution looks nice to me as it doesn't rely on third party code.Beaming
Alternatively, if you were to set the data attribute using element.attr(), it does then save to the DOM as opposed to memory. I had some problem using data so had to use the attr function.Jackson
@ClarenceLiu Because of this, I've made it a personal standard to always set data values using jQuery's .attr method instead of .data.Sinegold
A
100

I've created a new data selector that should enable you to do nested querying and AND conditions. Usage:

$('a:data(category==music,artist.name==Madonna)');

The pattern is:

:data( {namespace} [{operator} {check}]  )

"operator" and "check" are optional. So, if you only have :data(a.b.c) it will simply check for the truthiness of a.b.c.

You can see the available operators in the code below. Amongst them is ~= which allows regex testing:

$('a:data(category~=^mus..$,artist.name~=^M.+a$)');

I've tested it with a few variations and it seems to work quite well. I'll probably add this as a Github repo soon (with a full test suite), so keep a look out!

The code:

(function(){

    var matcher = /\s*(?:((?:(?:\\\.|[^.,])+\.?)+)\s*([!~><=]=|[><])\s*("|')?((?:\\\3|.)*?)\3|(.+?))\s*(?:,|$)/g;

    function resolve(element, data) {

        data = data.match(/(?:\\\.|[^.])+(?=\.|$)/g);

        var cur = jQuery.data(element)[data.shift()];

        while (cur && data[0]) {
            cur = cur[data.shift()];
        }

        return cur || undefined;

    }

    jQuery.expr[':'].data = function(el, i, match) {

        matcher.lastIndex = 0;

        var expr = match[3],
            m,
            check, val,
            allMatch = null,
            foundMatch = false;

        while (m = matcher.exec(expr)) {

            check = m[4];
            val = resolve(el, m[1] || m[5]);

            switch (m[2]) {
                case '==': foundMatch = val == check; break;
                case '!=': foundMatch = val != check; break;
                case '<=': foundMatch = val <= check; break;
                case '>=': foundMatch = val >= check; break;
                case '~=': foundMatch = RegExp(check).test(val); break;
                case '>': foundMatch = val > check; break;
                case '<': foundMatch = val < check; break;
                default: if (m[5]) foundMatch = !!val;
            }

            allMatch = allMatch === null ? foundMatch : allMatch && foundMatch;

        }

        return allMatch;

    };

}());
Arronarrondissement answered 24/5, 2010 at 9:25 Comment(13)
@J-P: Very sweet, I knew I could count on you! Headed to bed now, but I'll try it out tomorrow. Is it possible to do OR operations as well? I don't need it, just curious.Mcclure
@J-P: Also, I'm curious about your take on the other data selector solutions, pros/cons for yours vs. theirs. For instance, this plugin: plugins.jquery.com/project/dataSelector.Mcclure
It is possible to do OR operations, but it might be best to simply have two selectors, $("a:data(condition),a:data(orCondition)") ... it'll have the same effect. The more features you add, the slower it'll be. If the logic is complex, then use $(foo).filter(function(){...}).Arronarrondissement
The data selector you linked to seems okay... the implementation seems a bit lacking though.Arronarrondissement
@J-P: I think I found a bug in the AND operation. See the last alert() in this code: jsbin.com/ixadi3/2/editMcclure
Sorry about that. The return statement should have been return allMatch, not return foundMatch. Should be working now.Arronarrondissement
@J-P: Did you ever turn this into a github project? I'm running a test using jQuery 1.5.1 using the selector $("input:data(test>400)") on an input with the html5 attribute data-test="500" but the selector is returning nothing.Culch
Will $('a:data(category==music)') select the following anchor <a href="#" data-category="music">my link</a>?Standpipe
@JamesSouth That's because the selector in this answer uses the low level jQuery.data, which does not get the data defined in the HTML5 attributes. If you want that functionality, you can change jQuery.data to $('selector').data, but that's a trade off for speed.Slavism
I'm trying your code, but I can't make it works with this kind of field: data-my-value I tried with $('div:data(myValue==5213)') or $('div:data(my-value==5213)'), but both doesn't seems to worksFeticide
chrome throws unsupported pseudo: data error whenever I use :data selectorConcern
@999 This is great answer, thank you very much. What can you say about perfomance? Is it much slower then using data selector $('.class[data-smth="1"]) or the speed difference is so small, that I should don't take it into account?Adele
@Arronarrondissement I love your solution here and have been using it. I'm running into some performance issues. I have a question open (https://mcmap.net/q/137605/-performance-of-regex-within-jquery-data-selector-dependance-on-certain-string-length/1214731) if you're interested in following up.Aristate
L
82

You can also use a simple filtering function without any plugins. This is not exactly what you want but the result is the same:

$('a').data("user", {name: {first:"Tom",last:"Smith"},username: "tomsmith"});

$('a').filter(function() {
    return $(this).data('user') && $(this).data('user').name.first === "Tom";
});
Lashing answered 6/4, 2012 at 7:15 Comment(3)
This is a great idea, I forgot the filter traversal function could accept a test function =) thanksZachery
How could I do a "contains" instead of is equal to Tom?Probst
Alisso, instead of name.first == "Tom" use name.first && name.first.indexOF("Tom") > -1;Lashing
Z
24

I want to warn you that $('a[data-attribute=true]') doesn't work, as per Ashley's reply, if you attached data to a DOM element via the data() function.

It works as you'd expect if you added an actual data-attr in your HTML, but jQuery stores the data in memory, so the results you'd get from $('a[data-attribute=true]') would not be correct.

You'll need to use the data plugin http://code.google.com/p/jquerypluginsblog/, use Dmitri's filter solution, or do a $.each over all the elements and check .data() iteratively

Zachery answered 6/2, 2012 at 18:2 Comment(1)
In my case it's fine since I'm pre-populating the field on the server side and only need to consult it this way on initial load. Filter is probably just as fast (each is almost certainly slower) but this wins on readability.Morn
E
11

There's a :data() filter plugin that does just this :)

Some examples based on your question:

$('a:data("category=music")')
$('a:data("user.name.first=Tom")');
$('a:data("category=music"):data("artist.name=Madonna")');
//jQuery supports multiple of any selector to restrict further, 
//just chain with no space in-between for this effect

The performance isn't going to be extremely great compared to what's possible, selecting from $._cache and grabbing the corresponding elements is by far the fastest, but a lot more round-about and not very "jQuery-ey" in terms of how you get to stuff (you usually come in from the element side). Of th top of my head, I'm not sure this is fastest anyway since the process of going from unique Id to element is convoluted in itself, in terms of performance.

The comparison selector you mentioned will be best to do in a .filter(), there's no built-in support for this in the plugin, though you could add it in without a lot of trouble.

Elytron answered 23/5, 2010 at 11:20 Comment(2)
thanks for the thorough answer. Do you know if using HTML5 data-* attributes and selecting on them would be faster than selecting on .data() properties? Also, any idea where can I find out more about $._cache? I googled for it, but am not finding much.Mcclure
@Mcclure - My fault it's $.cache not $._cache, you can see how it's implemented and used in jQuery core here: github.com/jquery/jquery/blob/master/src/data.js#L4 When you call .data() it's actually storing it as an object in $.cache[elementUniqueID], which is an Id assigned as needed in an incrimental way to each element, e.g., 1, 2, 3, etc. That climbing ID will be exposed in jQuery 1.4.3 I believe, based off the git comments the other day. I would assume the HTML 5 route would be faster, depends what browser optimizations are available (I'm sure more will be come available).Elytron
T
7

You can set a data-* attribute on an elm using attr(), and then select using that attribute:

var elm = $('a').attr('data-test',123); //assign 123 using attr()
elm = $("a[data-test=123]"); //select elm using attribute

and now for that elm, both attr() and data() will yield 123:

console.log(elm.attr('data-test')); //123
console.log(elm.data('test')); //123

However, if you modify the value to be 456 using attr(), data() will still be 123:

elm.attr('data-test',456); //modify to 456
elm = $("a[data-test=456]"); //reselect elm using new 456 attribute

console.log(elm.attr('data-test')); //456
console.log(elm.data('test')); //123

So as I understand it, seems like you probably should steer clear of intermingling attr() and data() commands in your code if you don't have to. Because attr() seems to correspond directly with the DOM whereas data() interacts with the 'memory', though its initial value can be from the DOM. But the key point is that the two are not necessarily in sync at all.

So just be careful.

At any rate, if you aren't changing the data-* attribute in the DOM or in the memory, then you won't have a problem. Soon as you start modifying values is when potential problems can arise.

Thanks to @Clarence Liu to @Ash's answer, as well as this post.

Threescore answered 11/4, 2013 at 17:39 Comment(0)
C
7
$('a[data-category="music"]')

It works. See Attribute Equals Selector [name=”value”].

Crumple answered 18/6, 2015 at 6:44 Comment(0)
A
5

If you also use jQueryUI, you get a (simple) version of the :data selector with it that checks for the presence of a data item, so you can do something like $("div:data(view)"), or $( this ).closest(":data(view)").

See http://api.jqueryui.com/data-selector/ . I don't know for how long they've had it, but it's there now!

Alvar answered 11/1, 2013 at 12:53 Comment(1)
This only checks for the presence of the data item like you said. It doesn't check the values (according to the docs). Nice to know about this thoughVesuvius
S
3

Here's a plugin that simplifies life https://github.com/rootical/jQueryDataSelector

Use it like that:

data selector           jQuery selector
  $$('name')              $('[data-name]')
  $$('name', 10)          $('[data-name=10]')
  $$('name', false)       $('[data-name=false]')
  $$('name', null)        $('[data-name]')
  $$('name', {})          Syntax error
Swash answered 25/1, 2014 at 20:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.