jQuery: subtle difference between .has() and :has()
Asked Answered
M

2

10

When used with the child selector >, the two variants of jQuery's "has" behave differently.

Take this HTML:

<div>
  <span>Text</span>
</div>

Now:

$("div:has(>span)");

would return it, while:

$("div").has(">span");

would not. Is it a bug or a feature? Compare here: http://jsfiddle.net/aC9dP/


EDIT: This may be a bug or at least undocumented inconsistent behavior.

Anyway, I think it would be beneficial to have the child selector consistently work as an unary operator. It enables you to do something that otherwise would require a custom filter function — it lets you directly select elements that have certain children:

$("ul:has(>li.active)").show();     // works
$("ul").has(">li.active)").show();  // doesn't work, but IMHO it should

as opposed to:

$("ul").filter(function () {
  return $(this).children("li.active").length > 0;
}).show();

I've opened a jQuery ticket (7205) for this.

Maloriemalory answered 15/10, 2010 at 18:8 Comment(0)
I
7

This happens because the sizzle selector is looking at all Div's that have span children in the :has example. But in the .has example, it's passing all DIV's to the .has(), which then looks for something that shouldn't be a stand-alone selection. ("Has children of nothing").

Basically, :has() is part of the selection, but .has() gets passed those divs and then re-selects from them.

Ideally, you don't use selectors like this. The > being in the selector is probably a bug, as it's semantically awkward. Note: the child operator isn't meant to be stand-alone.

Sizzle vs target.sizzle:

I'm always talking about v1.4.2 of jquery development release.

.has (line 3748 of jQuery)

Description: Reduce the set of matched elements to those that have a descendant that matches the selector or DOM element.

Code:

    var targets = jQuery( target );
    return this.filter(function() {
        for ( var i = 0, l = targets.length; i < l; i++ ) {
            if ( jQuery.contains( this, targets[i] ) ) { //Calls line 3642
                return true;
            }
        }
    });

Line 3642 relates to a 2008 plugin compareDocumentPosition, but the important bit here is that we're now basically just running two jquery queries here, where the first one selects $("DIV") and the next one selects $(">span") (which returns null), then we check for children.

:has (line 3129 of jQuery)

Description: Selects elements which contain at least one element that matches the specified selector.

Code:

return !!Sizzle( match[3], elem ).length;

They are two differnt tools, the :has uses sizzle 100%, and .has uses targets passed to it.

Note: if you think this is a bug, go fill out the bug ticket.

Indelible answered 15/10, 2010 at 18:13 Comment(15)
That doesn't answer the question 'why', does it?Punkah
Also, I'm specifically looking for children only, or I would not use the > selector. ;-)Maloriemalory
Updated with why. Child selector should be using a parent anyway. api.jquery.com/child-selector :has is part of CSS selectors, where .has is not.Indelible
So this boils down to: "there is no way of expressing »has child« in jQuery's '.has()', and that it works in sizzle is by accident"…Maloriemalory
The statements "which contain at least one element" and "that have a descendant" are different in what regard, exactly? I don't think that this makes a very good argument. :-)Maloriemalory
So - can I express "has child" with jQuery without writing a custom filter function or relying on a feature that may be a bug (i.e. $("div:has(>span)"))?Maloriemalory
I'm adding some details, one moment.Indelible
what's the difference between !! and nothing?Essential
!! is a boolean double-NOT, which is useful for typecasting as boolean (improves speed, performance, etc..)Indelible
@Maloriemalory Done editing I think. Basically it's a 100% sizzle vs looping selections that don't match. I think this is a semantics bug with sizzle's :has, because > shouldn't even parse like this in the first place.Indelible
And yet it does. And it has its uses: You can select an element that has a certain direct child (in one selector). This is something that otherwise does not work. In a related note, this: $(">span") does not work, but this: $(">span", theDivElem) does.Maloriemalory
Curious now, Do you know if there's an official interpretation of an undefined's child element with this selector?Indelible
I would not say it's "an undefined's child" since the expression context is clear in all cases but a naked $(">span"), in which case a * can be assumed just like it is assumed when you do $(".class"). P.S.: I'll leave this question open some more. I've made a ticket, too (see question edit).Maloriemalory
My stance after looking around: A CSS2.1 grammar defiant syntax should not return anything. The fact that :has(">child") returns any selection is a bug as incorrect css2.1 grammar should return null or syntax error. Also, congratulations, my OCD is now in over-drive and I've forked the project :(. Hahaha :)Indelible
@user: Don't make me responsible! :-) From the ticket/forum it looks like the the jQuery folks tend to have the same opinion, so I might just as well accept this answer now. I would find .has(">span") a really nice feature, as selecting elements that have certain children is comparatively verbose otherwise. Probably it wouldn't break anything existing, either. Since one implementation (Sizzle) already does "the right thing", I'd of course like jQuery to do the same. :-) But then again, maybe it's just me.Maloriemalory
F
1

I think you may have hit upon a genuine bug. The problem may lie in the way you are using the child selector. As user257493 pointed out, it's not meant to be used on its own (or at least I don't see any examples of that in the documentation.

Check this out though. If you add a * before the child selector in the .has(), suddenly it works: http://jsfiddle.net/Ender/FjgZn/

But if you do the same thing in the :has() selector, it stops working! See here: http://jsfiddle.net/Ender/FjgZn/

There definitely seems to be difference in the way these two are implemented.

Fife answered 15/10, 2010 at 18:50 Comment(1)
Right, because .has is no longer matching null, but :has is matching div's that have spans nested inside any element. See here: jsfiddle.net/BDG/FjgZn/4Indelible

© 2022 - 2024 — McMap. All rights reserved.