querySelector search immediate children [duplicate]
Asked Answered
B

10

143

I have some jquery-like function:

function(elem) {
    return $('> someselector', elem);
};

The question is how can i do the same with querySelector()?

The problem is > selector in querySelector() requires parent to be explicitly specified. Is there any workaround?

Bloomers answered 26/6, 2011 at 1:36 Comment(0)
B
28

Complete :scope polyfill

As avetisk has mentioned Selectors API 2 uses :scope pseudo-selector.
To make this work in all browsers (that support querySelector) here is the polyfill

(function(doc, proto) {
  try { // check if browser supports :scope natively
    doc.querySelector(':scope body');
  } catch (err) { // polyfill native methods if it doesn't
    ['querySelector', 'querySelectorAll'].forEach(function(method) {
      var nativ = proto[method];
      proto[method] = function(selectors) {
        if (/(^|,)\s*:scope/.test(selectors)) { // only if selectors contains :scope
          var id = this.id; // remember current element id
          this.id = 'ID_' + Date.now(); // assign new unique id
          selectors = selectors.replace(/((^|,)\s*):scope/g, '$1#' + this.id); // replace :scope with #ID
          var result = doc[method](selectors);
          this.id = id; // restore previous id
          return result;
        } else {
          return nativ.call(this, selectors); // use native code for other selectors
        }
      }
    });
  }
})(window.document, Element.prototype);

Usage

node.querySelector(':scope > someselector');
node.querySelectorAll(':scope > someselector');

For historical reasons, my previous solution

Based on all answers

// Caution! Prototype extending
Node.prototype.find = function(selector) {
    if (/(^\s*|,\s*)>/.test(selector)) {
        if (!this.id) {
            this.id = 'ID_' + new Date().getTime();
            var removeId = true;
        }
        selector = selector.replace(/(^\s*|,\s*)>/g, '$1#' + this.id + ' >');
        var result = document.querySelectorAll(selector);
        if (removeId) {
            this.id = null;
        }
        return result;
    } else {
        return this.querySelectorAll(selector);
    }
};

Usage

elem.find('> a');
Bloomers answered 26/6, 2011 at 1:36 Comment(1)
Date.now() is not supported by older browsers, and could be replaced with new Date().getTime()Lint
U
202

Though it's not a full answer, you should keep an eye on the W3C Selector API v.2 which is already available in most browser, both desktop and mobile, except IE (Edge seems to support): see full support list.

function(elem) {
  return elem.querySelectorAll(':scope > someselector');
};
Urogenous answered 4/9, 2013 at 7:23 Comment(7)
This is the correct answer. However, browser support is limited and you'll need a shim if you want to use it. I built scopedQuerySelectorShim for this purpose.Televisor
The :scope attribute has been removed from the current specification.Delmerdelmor
Actually, :scope is still specified at drafts.csswg.org/selectors-4/#the-scope-pseudo. So far it's only the <style scoped> attribute that got removed; it's unclear whether that will also lead to :scope being removed (since <style scoped> was an important use case for :scope).Cashier
What a surprise: only Edge doesn't support this : )Garbers
In 2021 this is now supported in all modern browsers.Edgeways
Adding to @DisgruntledGoat's comment, modern browsers in this scopes means all major ones except IE, which already reached its End of Support anywaysWallas
There may be a little confusion about @tobi 's comment, to be clear scoped ATTRIBUTE was dropped, HOWEVER :scope PSEUDO-CLASS now has crossbrowser supportWenz
I
36

You can't. There's no selector that will simulate your starting point.

The way jQuery does it (more because of a way that qsa behaves that is not to their liking), is that they check to see if elem has an ID, and if not, they temporarily add an ID, then create a full selector string.

Basically you'd do:

var sel = '> someselector';
var hadId = true;
if( !elem.id ) {
    hadID = false;
    elem.id = 'some_unique_value';
}

sel = '#' + elem.id + sel;

var result = document.querySelectorAll( sel );

if( !hadId ) {
    elem.id = '';
}

This certainly isn't jQuery code, but from what I remember, it is basically what they do. Not just in this situation, but in any situation where you're running a selector from the context of a nested element.

Source code for Sizzle

Ironing answered 26/6, 2011 at 1:43 Comment(1)
Correct at the time of writing, but see @avetisk's answer for an updated method.Televisor
B
28

Complete :scope polyfill

As avetisk has mentioned Selectors API 2 uses :scope pseudo-selector.
To make this work in all browsers (that support querySelector) here is the polyfill

(function(doc, proto) {
  try { // check if browser supports :scope natively
    doc.querySelector(':scope body');
  } catch (err) { // polyfill native methods if it doesn't
    ['querySelector', 'querySelectorAll'].forEach(function(method) {
      var nativ = proto[method];
      proto[method] = function(selectors) {
        if (/(^|,)\s*:scope/.test(selectors)) { // only if selectors contains :scope
          var id = this.id; // remember current element id
          this.id = 'ID_' + Date.now(); // assign new unique id
          selectors = selectors.replace(/((^|,)\s*):scope/g, '$1#' + this.id); // replace :scope with #ID
          var result = doc[method](selectors);
          this.id = id; // restore previous id
          return result;
        } else {
          return nativ.call(this, selectors); // use native code for other selectors
        }
      }
    });
  }
})(window.document, Element.prototype);

Usage

node.querySelector(':scope > someselector');
node.querySelectorAll(':scope > someselector');

For historical reasons, my previous solution

Based on all answers

// Caution! Prototype extending
Node.prototype.find = function(selector) {
    if (/(^\s*|,\s*)>/.test(selector)) {
        if (!this.id) {
            this.id = 'ID_' + new Date().getTime();
            var removeId = true;
        }
        selector = selector.replace(/(^\s*|,\s*)>/g, '$1#' + this.id + ' >');
        var result = document.querySelectorAll(selector);
        if (removeId) {
            this.id = null;
        }
        return result;
    } else {
        return this.querySelectorAll(selector);
    }
};

Usage

elem.find('> a');
Bloomers answered 26/6, 2011 at 1:36 Comment(1)
Date.now() is not supported by older browsers, and could be replaced with new Date().getTime()Lint
W
9

If you want to eventually find direct children (and not e.g. > div > span), you can try Element.matches():

const elem = document.body
const elems = [...elem.children].filter(e => e.matches('b'))

console.log(elems)
<a>1</a>
<b>2</b>
<b>3</b>
<b>4</b>
<s>5</s>
Windowlight answered 1/11, 2018 at 12:38 Comment(0)
M
6

CLAIM

Personally I would take the answer from patrick dw, and +1 his answer, my answer is for seeking alternative solution. I don't think it deserves a downvote.

Here is my attempt :

function q(elem){
    var nodes = elem.querySelectorAll('someSeletor');
    console.log(nodes);
    for(var i = 0; i < nodes.length; i++){
        if(nodes[i].parentNode === elem) return nodes[i];
    }
}

see http://jsfiddle.net/Lgaw5/8/

Mosely answered 26/6, 2011 at 2:27 Comment(10)
Couple of problems with that. @Bloomers wants it to work with querySelector, which means that :eq() can't be used. Even if it could, your selector would return the element that is :eq() to its appearance on the page, not :eq() to the index of its parent (which is where you're getting the idx).Ironing
+1 about the :eq() & querySelector. And I should add the context $(elem).parent().Mosely
Unfortunately that won't work either. When the selector runs from the context of the parent, it starts with the left most child and finds all matching elements no matter how deeply nested, the continues on. So say the elem is at index 4, but there's a previous sibling that has a different tagName, but it has nested inside it an element with the matching tagName, that nested element will be included in the results before the one you're targeting, again throwing off the index. Here's an exampleIroning
...anyway, what you're ultimately doing is a more complex version of the code in the question, which does work. $('> someselector', elem); ;o)Ironing
Yes but you needed to hard code that single increment. That would require prior knowledge of it. If I add another similar element, it's broken again. ;o) jsfiddle.net/Lgaw5/3Ironing
well, good, but that's just for some thoughts for alternative solution, even it is not a good one, it doesn't deserve a down vote, right?Mosely
No, I think your latest answer looks better. Still checking it out. I wasn't the one who down-voted you.Ironing
You forgot to post the link. ;o) ...ah, you must mean your updated answer. :opIroning
Well that works, but I'm not sure how it relates to the question at this point.Ironing
Ok, I see what you're saying. (I was falling asleep last night.) Yes, that would seem to work. +1Ironing
I
5

If you know the tag name of the element that you’re looking into, then you can use it in the selector to achieve what you want.

For example if you have a <select> that has <option>s and <optgroups>, and you only want the <option>s that are its immediate children, not the ones inside <optgoups>:

<select>
  <option>iPhone</option>
  <optgroup>
    <option>Nokia</option>
    <option>Blackberry</option>
  </optgroup>
</select>

So, having a reference to the select element, you can — surprisingly — get its immediate children like this:

selectElement.querySelectorAll('select > option')

It seems to work in Chrome, Safari, and Firefox, but didn’t test in IEs. =/

Isomagnetic answered 18/10, 2015 at 17:17 Comment(2)
Warning: This answer is not actually limiting scope. If pattern is repeated deeper nest level it's still will be selected even if it's not direct children. <ul id="start"> <li>A</li> <li> <ul> <li>b</li> <li>c</li> </ul> </li> </ul> var result = document.getElementById('start').querySelectorAll('ul>li'); -> All 4 li elements will be selected!Unicellular
God forbid there was two <select> elements in the same parent.Cotta
C
2

The following is a simplified, generic method for running any CSS selector query over only direct children - it also accounts for combined queries, like "foo[bar], baz.boo":

var count = 0;
function queryChildren(element, selector) {
  var id = element.id,
      guid = element.id = id || 'query_children_' + count++,
      attr = '#' + guid + ' > ',
      selector = attr + (selector + '').replace(',', ',' + attr, 'g');
  var result = element.parentNode.querySelectorAll(selector);
  if (!id) element.removeAttribute('id');
  return result;
}


*** Example Use ***

queryChildren(someElement, '.foo, .bar[xyz="123"]');
Centiliter answered 29/7, 2013 at 18:36 Comment(0)
T
2

There’s a query-relative lib, which is quite handy replacement for query-selector. It polyfills children selector '> *' and :scope (inc. IE8), as well as normalizes :root selector. Also it provides some special relative pseudos like :closest, :parent, :prev, :next, just in case.

Thordia answered 9/12, 2014 at 23:10 Comment(0)
V
1

That worked for me:

Node.prototype.search = function(selector)
{
    if (selector.indexOf('@this') != -1)
    {
        if (!this.id)
            this.id = "ID" + new Date().getTime(); 
        while (selector.indexOf('@this') != -1)
            selector = selector.replace('@this', '#' + this.id);
        return document.querySelectorAll(selector);
    } else 
        return this.querySelectorAll(selector);
};

you will have to pass the @this keywork before the > when you want to search for immediate children.

Velour answered 16/7, 2012 at 20:34 Comment(1)
Seems the same as the currenty accepted answer... Btw, what's the point of weird "@this" syntax? Why not just test for ">" with regexp?Bloomers
M
0

check if element have id else add random id and do search based on it

function(elem) {
      if(!elem.id)
          elem.id = Math.random().toString(36).substr(2, 10);
      return elem.querySelectorAll(elem.id + ' > someselector');
    };

will do same thing as

$("> someselector",$(elem))
Mainsail answered 26/2, 2015 at 20:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.