Utilisation de querySelectorAll pour récupérer les enfants directs

119

Je suis capable de faire ceci:

<div id="myDiv">
   <div class="foo"></div>
</div>
myDiv = getElementById("myDiv");
myDiv.querySelectorAll("#myDiv > .foo");

Autrement dit, je peux récupérer avec succès tous les enfants directs de l' myDivélément qui ont la classe .foo.

Le problème, c'est que cela me dérange de devoir inclure le #myDivdans le sélecteur, car j'exécute la requête sur l' myDivélément (donc c'est évidemment redondant).

Je devrais pouvoir laisser le #myDivdésactivé, mais le sélecteur n'est pas une syntaxe légale puisqu'il commence par un >.

Quelqu'un sait-il comment écrire un sélecteur qui n'obtient que les enfants directs de l'élément sur lequel le sélecteur s'exécute?

mattsh
la source
Aucune des réponses n'a-t-elle accompli ce dont vous aviez besoin? Veuillez fournir des commentaires ou sélectionner une réponse.
Randy Hall
@mattsh - J'ai ajouté une meilleure réponse (IMO) à votre besoin, vérifiez-la!
csuwldcat

Réponses:

85

Bonne question. Au moment où cela a été demandé, il n'existait pas de moyen universellement implémenté de faire des «requêtes enracinées par combinateur» (comme les appelait John Resig ).

Maintenant, la pseudo-classe : scope a été introduite. Il n'est pas pris en charge sur les versions [pré-Chrominum] d'Edge ou d'IE, mais est déjà pris en charge par Safari depuis quelques années. En utilisant cela, votre code pourrait devenir:

let myDiv = getElementById("myDiv");
myDiv.querySelectorAll(":scope > .foo");

Notez que dans certains cas, vous pouvez également ignorer .querySelectorAllet utiliser d'autres bonnes fonctionnalités de l'API DOM à l'ancienne. Par exemple, au lieu de myDiv.querySelectorAll(":scope > *")vous pourriez simplement écrire myDiv.children, par exemple.

Sinon, si vous ne pouvez pas encore vous fier :scope, je ne peux pas penser à une autre façon de gérer votre situation sans ajouter plus de logique de filtre personnalisé (par exemple, trouver à myDiv.getElementsByClassName("foo")qui .parentNode === myDiv), et évidemment pas idéal si vous essayez de prendre en charge un chemin de code qui vraiment veut juste prendre une chaîne de sélection arbitraire en entrée et une liste de correspondances en sortie! Mais si, comme moi, vous avez fini par poser cette question simplement parce que vous êtes resté coincé en pensant que "tout ce que vous aviez était un marteau", n'oubliez pas qu'il existe une variété d' autres outils que le DOM propose également.

natevw
la source
3
myDiv.getElementsByClassName("foo")n'est pas la même chose que myDiv.querySelectorAll("> .foo"), c'est plus comme myDiv.querySelectorAll(".foo")(ce qui fonctionne en fait) en ce sens qu'il trouve tous les descendants .foopar opposition à seulement les enfants.
BoltClock
Oh frere! Je veux dire par là: vous avez tout à fait raison. J'ai mis à jour ma réponse pour préciser que ce n'est pas une très bonne réponse dans le cas du PO.
natevw
merci pour le lien (qui semble y répondre définitivement), et merci d'avoir ajouté la terminologie "requêtes enracinées par combinateur".
mattsh
1
Ce que John Resig appelle des «requêtes enracinées dans un combinateur», Selectors 4 les appelle désormais sélecteurs relatifs .
BoltClock
Les feuilles de style @Andrew Scoped (ie <style scoped>) n'ont rien à voir avec le :scopepseudo-sélecteur décrit dans cette réponse.
natevw
124

Quelqu'un sait-il comment écrire un sélecteur qui n'obtient que les enfants directs de l'élément sur lequel le sélecteur s'exécute?

La manière correcte d'écrire un sélecteur qui est "enraciné" sur l'élément courant est d'utiliser :scope.

var myDiv = getElementById("myDiv");
var fooEls = myDiv.querySelectorAll(":scope > .foo");

Cependant, la prise en charge du navigateur est limitée et vous aurez besoin d'un shim si vous souhaitez l'utiliser. J'ai construit scopedQuerySelectorShim à cet effet.

paresseux
la source
3
Il convient de mentionner que la :scopespécification est actuellement un «projet de travail» et donc sujette à changement. Il est probable que cela fonctionnera toujours comme ça si / quand il sera adopté, mais un peu tôt pour dire que c'est la "bonne façon" de le faire à mon avis.
Zach Lysobey
2
On dirait qu'il a été obsolète entre
Adrian Moisa
1
3 ans plus tard et vous sauvez mon grand fessier!
Mehrad
5
@Adrian Moisa:: scope n'a été abandonné nulle part. Vous le confondez peut-être avec l'attribut scoped.
BoltClock
6

Voici une méthode flexible, écrite en vanilla JS, qui vous permet d'exécuter une requête de sélection CSS uniquement sur les enfants directs d'un élément:

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;
}
csuwldcat
la source
Je vous suggère d'ajouter une sorte d'horodatage ou de compteur aux identifiants que vous générez. Il y a une probabilité non nulle (bien que minime) pour Math.random().toString(36).substr(2, 10)produire le même jeton plus d'une fois.
Frédéric Hamidi
Je ne sais pas quelle est la probabilité que la probabilité de répétition des hachages soit minuscule, l'ID n'est appliqué que temporairement pendant une fraction de milliseconde, et toute dupe devrait également être un enfant du même nœud parent - en gros, le les chances sont astronomiques. Quoi qu'il en soit, l'ajout d'un compteur semble bien.
csuwldcat
Je ne sais pas ce que vous entendez par là any dupe would need to also be a child of the same parent node, les idattributs concernent l’ensemble du document. Vous avez raison, les chances sont encore assez négligeables, mais merci d'avoir pris la haute route et d'ajouter ce compteur :)
Frédéric Hamidi
8
JavaScript n'est pas exécuté en parallèle, donc les chances sont en fait nulles.
WGH
1
@WGH Wow, je ne peux pas y croire, je suis absolument abasourdi de péter le cerveau sur un point aussi simple (je travaille chez Mozilla et je le récite souvent, je dois dormir plus! LOL)
csuwldcat
5

si vous savez avec certitude que l'élément est unique (comme votre cas avec l'ID):

myDiv.parentElement.querySelectorAll("#myDiv > .foo");

Pour une solution plus "globale": (utilisez un shim matchesSelector )

function getDirectChildren(elm, sel){
    var ret = [], i = 0, l = elm.childNodes.length;
    for (var i; i < l; ++i){
        if (elm.childNodes[i].matchesSelector(sel)){
            ret.push(elm.childNodes[i]);
        }
    }
    return ret;
}

elmest votre élément parent et selest votre sélecteur. Pourrait totalement également être utilisé comme prototype.

Randy Hall
la source
@lazd qui ne fait pas partie de la question.
Randy Hall
Votre réponse n'est pas une solution «globale». Il a des limites spécifiques qu'il convient de noter, d'où mon commentaire.
lazd le
@lazd qui répond à la question posée. Alors pourquoi le vote négatif? Ou avez-vous simplement fait des commentaires quelques secondes après le vote défavorable sur la réponse vieille d'un an?
Randy Hall
La solution utilise un matchesSelectorpréfixe (qui ne fonctionne même pas dans la dernière version de Chrome), elle pollue l'espace de noms global (ret n'a pas été déclaré), elle ne retourne pas une NodeList comme on attend de querySelectorMethods. Je ne pense pas que ce soit une bonne solution, d'où le vote négatif.
lazd
@lazd - La question d'origine utilise une fonction sans préfixe et un espace de noms global. C'est une solution de contournement parfaitement fine pour la question posée. Si vous ne pensez pas que ce soit une bonne solution, ne l'utilisez pas ou suggérez une modification. Si c'est fonctionnellement incorrect ou spam, envisagez un vote défavorable.
Randy Hall
2

La solution suivante est différente de celles proposées jusqu'à présent et fonctionne pour moi.

La raison est que vous sélectionnez d'abord tous les enfants correspondants, puis que vous filtrez ceux qui ne sont pas des enfants directs. Un enfant est un enfant direct s'il n'a pas de parent correspondant avec le même sélecteur.

function queryDirectChildren(parent, selector) {
    const nodes = parent.querySelectorAll(selector);
    const filteredNodes = [].slice.call(nodes).filter(n => 
        n.parentNode.closest(selector) === parent.closest(selector)
    );
    return filteredNodes;
}

HTH!

Melle
la source
J'aime cela pour sa brièveté et la simplicité de l'approche, même si elle ne fonctionnerait probablement pas bien si elle était appelée à haute fréquence avec de grands arbres et des sélecteurs trop gourmands.
brennanyoung
1

J'ai créé une fonction pour gérer cette situation, j'ai pensé la partager.

getDirectDecendent(elem, selector, all){
    const tempID = randomString(10) //use your randomString function here.
    elem.dataset.tempid = tempID;

    let returnObj;
    if(all)
        returnObj = elem.parentElement.querySelectorAll(`[data-tempid="${tempID}"] > ${selector}`);
    else
        returnObj = elem.parentElement.querySelector(`[data-tempid="${tempID}"] > ${selector}`);

    elem.dataset.tempid = '';
    return returnObj;
}

En substance, ce que vous faites est de générer une chaîne aléatoire (la fonction randomString ici est un module npm importé, mais vous pouvez créer le vôtre.) Puis d'utiliser cette chaîne aléatoire pour garantir que vous obtenez l'élément que vous attendez dans le sélecteur. Ensuite, vous êtes libre d'utiliser >après cela.

La raison pour laquelle je n'utilise pas l'attribut id est que l'attribut id est peut-être déjà utilisé et je ne veux pas le remplacer.

cpi
la source
0

Eh bien, nous pouvons facilement obtenir tous les enfants directs d'un élément en utilisant childNodeset nous pouvons sélectionner des ancêtres avec une classe spécifique avec querySelectorAll, il n'est donc pas difficile d'imaginer que nous pourrions créer une nouvelle fonction qui obtient les deux et compare les deux.

HTMLElement.prototype.queryDirectChildren = function(selector){
  var direct = [].slice.call(this.directNodes || []); // Cast to Array
  var queried = [].slice.call(this.querySelectorAll(selector) || []); // Cast to Array
  var both = [];
  // I choose to loop through the direct children because it is guaranteed to be smaller
  for(var i=0; i<direct.length; i++){
    if(queried.indexOf(direct[i])){
      both.push(direct[i]);
    }
  }
  return both;
}

Remarque: Cela renverra un tableau de nœuds, pas une liste de nœuds.

Usage

 document.getElementById("myDiv").queryDirectChildren(".foo");
Dustin Poissant
la source
0

Je voudrais ajouter que vous pouvez étendre la compatibilité de : scope en attribuant simplement un attribut temporaire au nœud actuel.

let node = [...];
let result;

node.setAttribute("foo", "");
result = window.document.querySelectorAll("[foo] > .bar");
// And, of course, you can also use other combinators.
result = window.document.querySelectorAll("[foo] + .bar");
result = window.document.querySelectorAll("[foo] ~ .bar");
node.removeAttribute("foo");
Vasile Alexandru Peşte
la source
-1

Je serais allé avec

var myFoo = document.querySelectorAll("#myDiv > .foo");
var myDiv = myFoo.parentNode;
Lee Chase
la source
-2

Je fais juste cela sans même essayer. Cela fonctionnerait-il?

myDiv = getElementById("myDiv");
myDiv.querySelectorAll(this.id + " > .foo");

Essayez-le, peut-être que cela fonctionne peut-être pas. Apolovies, mais je ne suis pas sur un ordinateur maintenant pour l'essayer (réponse depuis mon iPhone).

Greeso
la source
3
QSA est limité à l'élément sur lequel il est appelé, donc cela chercherait un autre élément dans myDiv avec le même ID que myDiv, puis ses enfants avec .foo. Vous pouvez faire quelque chose comme `document.querySelectorAll ('#' + myDiv.id + '> .foo');
probablement le