views:

181

answers:

2

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?

+2  A: 

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.

Nick Craver
@Nick, 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.
Tauren
@Tauren - My fault it's `$.cache` not `$._cache`, you can see how it's implemented and used in jQuery core here: http://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).
Nick Craver
@Nick: great, thanks for the help!
Tauren
+3  A: 

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;

    };

}());
J-P
@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.
Tauren
@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: http://plugins.jquery.com/project/dataSelector.
Tauren
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(){...})`.
J-P
The data selector you linked to seems okay... the implementation seems a bit lacking though.
J-P
@J-P: I think I found a bug in the AND operation. See the last alert() in this code: http://jsbin.com/ixadi3/2/edit
Tauren
Sorry about that. The return statement should have been `return allMatch`, not `return foundMatch`. Should be working now.
J-P
@J-P: Perfect, that solved it! Thanks.
Tauren