Usando querySelectorAll para recuperar hijos directos

119

Puedo hacer esto:

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

Es decir, puedo recuperar con éxito todos los hijos directos del myDivelemento que tienen clase .foo.

El problema es que me molesta que deba incluir el #myDiven el selector, porque estoy ejecutando la consulta en el myDivelemento (por lo que obviamente es redundante).

Debería poder dejar la opción #myDivdesactivada, pero entonces el selector no es una sintaxis legal ya que comienza con >.

¿Alguien sabe cómo escribir un selector que obtenga solo los hijos directos del elemento en el que se está ejecutando el selector?

mattsh
fuente
¿Ninguna de las respuestas logró lo que necesitaba? Envíe sus comentarios o seleccione una respuesta.
Randy Hall
@mattsh: agregué una respuesta mejor (en mi opinión) para sus necesidades, ¡compruébelo!
csuwldcat

Respuestas:

85

Buena pregunta. En el momento en que se solicitó, no existía una forma implementada universalmente para realizar "consultas con raíz de combinador" (como las llamó John Resig ).

Ahora se ha introducido la pseudoclase: scope . No es compatible con las versiones [anteriores a Chrominum] de Edge o IE, pero Safari ya lo ha admitido durante algunos años. Usando eso, su código podría convertirse en:

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

Tenga en cuenta que, en algunos casos, también puede omitir .querySelectorAlly utilizar otras características de la API DOM antiguas. Por ejemplo, en lugar de myDiv.querySelectorAll(":scope > *")simplemente escribir myDiv.children, por ejemplo.

De lo contrario, si aún no puede confiar :scope, no puedo pensar en otra forma de manejar su situación sin agregar más lógica de filtro personalizada (por ejemplo, encontrar de myDiv.getElementsByClassName("foo")quién .parentNode === myDiv), y obviamente no es ideal si está tratando de admitir una ruta de código que realmente ¡solo quiere tomar una cadena de selección arbitraria como entrada y una lista de coincidencias como salida! Pero si, como yo, terminaste haciendo esta pregunta simplemente porque te quedaste atascado pensando "todo lo que tenías fue un martillo", no olvides que hay una variedad de otras herramientas que DOM también ofrece.

natevw
fuente
3
myDiv.getElementsByClassName("foo")no es lo mismo que myDiv.querySelectorAll("> .foo"), es más parecido myDiv.querySelectorAll(".foo")(que en realidad funciona, por cierto) en el sentido de que encuentra todos los descendientes .fooen lugar de solo los hijos.
BoltClock
¡Oh hermano! Con lo que quiero decir: tienes toda la razón. Actualicé mi respuesta para dejar más claro que no es muy buena en el caso del OP.
natevw
gracias por el enlace (que parece responder definitivamente), y gracias por agregar la terminología "consultas arraigadas del combinador".
mattsh
1
Lo que John Resig llama "consultas con raíz de combinador", Selectores 4 ahora las llama selectores relativos .
BoltClock
Las hojas de estilo de @Andrew Scoped (es decir <style scoped>) no tienen nada que ver con el :scopepseudo-selector descrito en esta respuesta.
natevw
124

¿Alguien sabe cómo escribir un selector que obtenga solo los hijos directos del elemento en el que se está ejecutando el selector?

La forma correcta de escribir un selector que está "arraigado" al elemento actual es usar :scope.

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

Sin embargo, la compatibilidad con el navegador es limitada y necesitará una cuña si desea utilizarla. Construí scopedQuerySelectorShim para este propósito.

lazd
fuente
3
Vale la pena mencionar que la :scopeespecificación es actualmente un "Borrador de trabajo" y, por lo tanto, está sujeta a cambios. Es probable que siga funcionando así si / cuando se adopte, pero un poco antes de decir que esta es la "forma correcta" de hacerlo en mi opinión.
Zach Lysobey
2
Mientras tanto
Adrian Moisa
1
¡3 años después y salvas mi glúteo mayor!
Mehrad
5
@Adrian Moisa:: el alcance no ha quedado obsoleto en ninguna parte. Es posible que lo esté confundiendo con el atributo de ámbito.
BoltClock
6

Aquí hay un método flexible, escrito en vanilla JS, que le permite ejecutar una consulta de selector de CSS solo sobre los hijos directos de un elemento:

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
fuente
Le sugiero que agregue algún tipo de marca de tiempo o contador a los identificadores que genera. Existe una probabilidad distinta de cero (aunque pequeña) de Math.random().toString(36).substr(2, 10)producir la misma ficha más de una vez.
Frédéric Hamidi
No estoy seguro de qué tan probable es que las probabilidades de repetir hashes sean minúsculas, la ID solo se aplica temporalmente durante una fracción de milisegundo, y cualquier duplicado también debería ser un hijo del mismo nodo principal, básicamente, el las posibilidades son astronómicas. Independientemente, agregar un contador parece estar bien.
csuwldcat
No está seguro de lo que entendemos por any dupe would need to also be a child of the same parent node, idatributos son en todo el documento. Tienes razón, las probabilidades siguen siendo bastante insignificantes, pero gracias por tomar el camino correcto y agregar ese contador :)
Frédéric Hamidi
8
JavaScript no se ejecuta en paralelo, por lo que las posibilidades son cero.
WGH
1
@WGH Wow, no puedo creerlo, estoy absolutamente sorprendido de que me tirara un pedo cerebral en un punto tan simple (trabajo en Mozilla y recito este hecho a menudo, ¡
tengo que
5

si sabe con certeza que el elemento es único (como su caso con el ID):

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

Para una solución más "global": (use un ajuste de MatchSelector )

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;
}

donde elmestá su elemento padre y seles su selector. También se podría utilizar totalmente como prototipo.

Randy Hall
fuente
@lazd eso no es parte de la pregunta.
Randy Hall
Su respuesta no es una solución "global". Tiene limitaciones específicas que deben tenerse en cuenta, de ahí mi comentario.
lazd
@lazd que responde a la pregunta formulada. Entonces, ¿por qué el voto negativo? ¿O simplemente hizo un comentario a los pocos segundos de la votación en contra de la respuesta del año anterior?
Randy Hall
La solución utiliza un matchesSelectorprefijo (que ni siquiera funciona en la última versión de Chrome), contamina el espacio de nombres global (no se declaró ret), no devuelve una NodeList como se espera que haga querySelectorMethods. No creo que sea una buena solución, de ahí el voto negativo.
Lazd
@lazd: la pregunta original usa una función sin prefijo y un espacio de nombres global. Es una solución perfecta para la pregunta dada. Si no cree que sea una buena solución, no la use ni sugiera una edición. Si es funcionalmente incorrecto o spam, entonces considere un voto negativo.
Randy Hall
2

La siguiente solución es diferente a las propuestas hasta ahora y me funciona.

La razón es que primero selecciona todos los hijos coincidentes y luego filtra los que no son hijos directos. Un hijo es un hijo directo si no tiene un padre que coincida con el mismo selector.

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
fuente
Me gusta esto por su brevedad y la simplicidad del enfoque, aunque lo más probable es que no funcione bien si se llama a alta frecuencia con árboles grandes y selectores demasiado codiciosos.
brennanyoung
1

Creé una función para manejar esta situación, pensé en compartirla.

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 esencia, lo que está haciendo es generar una cadena aleatoria (la función randomString aquí es un módulo npm importado, pero puede crear el suyo) y luego usar esa cadena aleatoria para garantizar que obtiene el elemento que espera en el selector. Entonces eres libre de usar el >después de eso.

La razón por la que no estoy usando el atributo id es que es posible que el atributo id ya esté en uso y no quiero anularlo.

cpi
fuente
0

Bueno, podemos obtener fácilmente todos los hijos directos de un elemento usando childNodesy podemos seleccionar ancestros con una clase específica con querySelectorAll, por lo que no es difícil imaginar que podríamos crear una nueva función que obtenga ambos y los compare.

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;
}

Nota: Esto devolverá una matriz de nodos, no una NodeList.

Uso

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

Me gustaría agregar que puede extender la compatibilidad de : scope simplemente asignando un atributo temporal al nodo actual.

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
fuente
-1

Me hubiera ido con

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

Estoy haciendo esto sin siquiera intentarlo. ¿Funcionaría esto?

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

Pruébelo, tal vez funcione, tal vez no. Disculpas, pero ahora no estoy en una computadora para probarlo (respondiendo desde mi iPhone).

Greeso
fuente
3
QSA tiene como alcance el elemento al que se llama, por lo que buscaría otro elemento dentro de myDiv con el mismo ID que myDiv, luego sus hijos con .foo. Podría hacer algo como `document.querySelectorAll ('#' + myDiv.id + '> .foo');
probablemente hasta el