James Allardice
James Allardice

Reputation: 165961

Pseudo-selectors with siblings method

Earlier, I answered this question, which was basically about removing a table row. This question came about as the result of the comments on that question. Given the following HTML:

<div><a href="#" class="removelink">remove</a></div>
<table>
    <tr>
        <td>Row 1</td> 
    </tr>
</table>

And the following jQuery:

$('.removelink').click(function(){
    $(this).parent().siblings('table tr:last').remove();
});

I would expect nothing to happen, because the siblings method should select the siblings of the currently matched element, optionally filtered by a selector. From the jQuery docs:

The method optionally accepts a selector expression of the same type that we can pass to the $() function. If the selector is supplied, the elements will be filtered by testing whether they match it.

Based on that, I read the above code as "get the siblings of the current element (the div) which are the last tr within a table". Obviously there are no elements that match that description - there is a tr within a table, but it's not a sibling of the div. So, I wouldn't expect any elements to be returned. However, it actually returns the entire table, as if it ignores the tr:last part of the selector entirely.

What confused me further was that if you remove the :last pseudo-selector, it works as expected (returning no elements).

Why is the entire table removed by the above code? Am I just being stupid and missing something obvious? You can see the above code in action here.

Edit - Here's a simplified version. Given the following HTML:

<div id="d1"></div>
<div>
    <span></span>
</div>

Why does the following jQuery return the second div:

$("#d1").siblings("div span:last");

I would expect it to return nothing, as there is not a span which is a sibling of #d1. Here's a fiddle for this simplified example.

Update

Following the brilliant investigation from @muistooshort, I have created a jQuery bug ticket to track this issue.

Upvotes: 4

Views: 1819

Answers (2)

mu is too short
mu is too short

Reputation: 434635

Allow me to expand on my comment a little bit. All of this is based on your second simplified example and jQuery 1.6.4. This is a little long winded perhaps but we need to walk through the jQuery code to find out what it is doing.


We do have the jQuery source available so let us go a wandering through it and see what wonders there are to behold therein.

The guts of siblings looks like this:

siblings: function( elem ) {
    return jQuery.sibling( elem.parentNode.firstChild, elem );
}

wrapped up in this:

// `name` is "siblings", `fn` is the function above.
jQuery.fn[ name ] = function( until, selector ) {
    var ret = jQuery.map( this, fn, until )

    //...

    if ( selector && typeof selector === "string" ) {
        ret = jQuery.filter( selector, ret );
    }

    //...
};

And then jQuery.sibling is this:

sibling: function( n, elem ) {
    var r = [];

    for ( ; n; n = n.nextSibling ) {
        if ( n.nodeType === 1 && n !== elem ) {
            r.push( n );
        }
    }

    return r;
}

So we go up one step in the DOM, go to the parent's first child, and continue sideways to get all of the parent's children (except the node we started at!) as an array of DOM elements.

That leaves us with all of our sibling DOM elements in ret and now to look at the filtering:

ret = jQuery.filter( selector, ret );

So what is filter all about? filter is all about this:

filter: function( expr, elems, not ) {
    //...
    return elems.length === 1 ?
        jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] :
        jQuery.find.matches(expr, elems);
}

In your case, elems will have have exactly one element (as #d1 has one sibling) so we're off to jQuery.find.matchesSelector which is actually Sizzle.matchesSelector:

var html = document.documentElement,
    matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector;
//...
Sizzle.matchesSelector = function( node, expr ) {
    // Make sure that attribute selectors are quoted
    expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']");

    if ( !Sizzle.isXML( node ) ) {
        try {
            if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) {
                var ret = matches.call( node, expr );

                // IE 9's matchesSelector returns false on disconnected nodes
                if ( ret || !disconnectedMatch ||
                        // As well, disconnected nodes are said to be in a document
                        // fragment in IE 9, so check for that
                        node.document && node.document.nodeType !== 11 ) {
                    return ret;
                }
            }
        } catch(e) {}
    }

    return Sizzle(expr, null, null, [node]).length > 0;
};

A bit of experimentation indicates that neither the Gecko nor WebKit versions of matchesSelector can handle div span:first so we end up in the final Sizzle() call; note that both the Gecko and WebKit matchesSelector variants can handle div span and your jsfiddles work as expected in the div span case.

What does Sizzle(expr, null, null, [node]) do? Why it returns an array containing the <span> inside your <div> of course. We'll have this in expr:

'div span:last'

and this in node:

<div id="d2">
    <span id="s1"></span>
</div>

So the <span id="s1"> inside node nicely matches the selector in expr and the Sizzle() call returns an array containing the <span> and since that array has a non-zero length, the matchesSelector call returns true and everything falls apart in a pile of nonsense.

The problem is that jQuery isn't interfacing with Sizzle properly in this case. Congratulations, you are the proud father of a bouncing baby bug.

Here's a (massive) jsfiddle with an inlined version of jQuery with a couple console.log calls to support what I'm talking about above:

http://jsfiddle.net/ambiguous/TxGXv/

A few things to note:

  1. You will get sensible results with div span and div span:nth-child(1); both of these use the native Gecko and WebKit selector engine.
  2. You will get the same broken results with div span:first, div span:last, and even div span:eq(0); all three of these go through Sizzle.
  3. The four argument version of the Sizzle() call that is being used not documented (see Public API) so we don't know if jQuery or Sizzle is at fault here.

Upvotes: 5

Sanooj
Sanooj

Reputation: 2637

Update with this

$('.removelink').click(function(){

$(this).parent().siblings('table').find('tr:last').remove();

});

Check Fiddle Example

Upvotes: 0

Related Questions