Usando querySelectorAll para recuperar filhos diretos


119

Eu sou capaz de fazer isso:

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

Ou seja, posso recuperar com êxito todos os filhos diretos do myDivelemento que possuem classe .foo.

O problema é que me incomoda que eu deva incluir o #myDivno seletor, porque estou executando a consulta no myDivelemento (por isso é obviamente redundante).

Eu deveria ser capaz de deixar o #myDivdesligado, mas o seletor não é uma sintaxe válida, pois começa com a >.

Alguém sabe escrever um seletor que obtém apenas os filhos diretos do elemento em que o seletor está sendo executado?



Nenhuma das respostas atendeu ao que você precisava? Forneça comentários ou selecione uma resposta.
Randy Hall

@mattsh - Eu adicionei uma resposta (IMO) melhor para sua necessidade, verifique!
csuwldcat

Respostas:


85

Boa pergunta. Na época em que foi perguntado, não existia uma maneira universalmente implementada de fazer "consultas com raiz de combinador" (como John Resig as chamou ).

Agora a pseudoclasse: scope foi introduzida. Não é compatível com versões [pré-Chrominum] do Edge ou IE, mas já é compatível com o Safari há alguns anos. Usando isso, seu código pode se tornar:

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

Observe que, em alguns casos, você também pode ignorar .querySelectorAlle usar outros bons e antigos recursos da API DOM. Por exemplo, em vez de myDiv.querySelectorAll(":scope > *")você pode apenas escrever myDiv.children, por exemplo.

Caso contrário, se você ainda não pode confiar em :scope, não consigo pensar em outra maneira de lidar com sua situação sem adicionar mais lógica de filtro personalizado (por exemplo, encontrar de myDiv.getElementsByClassName("foo")quem .parentNode === myDiv), e obviamente não é ideal se você está tentando oferecer suporte a um caminho de código que realmente só quer pegar uma string de seletor arbitrário como entrada e uma lista de correspondências como saída! Mas se, como eu, você acabou fazendo essa pergunta simplesmente porque parou pensando "tudo o que você tinha era um martelo", não se esqueça de que há uma variedade de outras ferramentas que o DOM oferece também.


3
myDiv.getElementsByClassName("foo")não é o mesmo que myDiv.querySelectorAll("> .foo"), é mais parecido com myDiv.querySelectorAll(".foo")(o que na verdade funciona) no sentido de que encontra todos os descendentes .foos em vez de apenas filhos.
BoltClock

Oh, que chatice! Com o que quero dizer: você está exatamente certo. Atualizei minha resposta para deixar mais claro que não é muito boa no caso do OP.
natevw

obrigado pelo link (que parece respondê-lo definitivamente), e obrigado por adicionar a terminologia "combinator rooted queries".
mattsh

1
O que John Resig chama de "consultas com raiz combinada", os Seletores 4 agora os chamam de seletores relativos .
BoltClock

@Andrew Scoped stylesheets (ie <style scoped>) não tem nada a ver com o :scopepseudo-seletor descrito nesta resposta.
natevw

124

Alguém sabe escrever um seletor que obtém apenas os filhos diretos do elemento em que o seletor está sendo executado?

A maneira correta de escrever um seletor "enraizado" no elemento atual é usar :scope.

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

No entanto, o suporte do navegador é limitado e você precisará de um calço se quiser usá-lo. Eu construí scopedQuerySelectorShim para esta finalidade.


3
Vale a pena mencionar que a :scopeespecificação é atualmente um "Rascunho de Trabalho" e, portanto, sujeita a alterações. É provável que ainda funcione assim se / quando for adotado, mas um pouco cedo para dizer que esta é a "maneira correta" de fazê-lo na minha opinião.
Zach Lysobey de

2
Parece que ele foi suspenso por enquanto
Adrian Moisa,

1
3 anos depois e você salva meu glúteo máximo!
Mehrad

5
@Adrian Moisa:: o escopo não foi descontinuado em lugar algum. Você pode estar confundindo-o com o atributo de escopo.
BoltClock

6

Este é um método flexível, escrito em vanilla JS, que permite executar uma consulta de seletor CSS apenas nos filhos diretos de um 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;
}

Eu sugiro que você adicione algum tipo de carimbo de data / hora ou contador aos ids que você gerar. Existe uma probabilidade diferente de zero (embora pequena) Math.random().toString(36).substr(2, 10)de produzir o mesmo token mais de uma vez.
Frédéric Hamidi

Não tenho certeza de quão provável é, dada a probabilidade de repetição de hashes ser minúscula, o ID é aplicado apenas temporariamente por uma fração de milissegundo e qualquer ingênuo também precisaria ser filho do mesmo nó pai - basicamente, o as chances são astronômicas. Independentemente disso, adicionar um contador parece bom.
csuwldcat

Não sei o que você quer dizer com any dupe would need to also be a child of the same parent node, idatributos são-wide documento. Você está certo, as chances ainda são mínimas, mas obrigado por seguir o caminho certo e adicionar aquele contador :)
Frédéric Hamidi

8
JavaScript não é executado em paralelo, então as chances são zero.
WGH

1
@WGH Uau, não posso acreditar, estou absolutamente pasmo de que iria peidar mentalmente sobre um ponto tão simples (eu trabalho na Mozilla e recito esse fato com frequência, tenho que dormir mais! LOL)
csuwldcat

5

se você tiver certeza de que o elemento é único (como seu caso com o ID):

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

Para uma solução mais "global": (use um shim 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;
}

onde elmestá o seu elemento pai e selé o seu seletor. Pode ser totalmente usado como protótipo também.


@lazd isso não faz parte da questão.
Randy Hall

Sua resposta não é uma solução "global". Possui limitações específicas que devem ser observadas, daí meu comentário.
tarde

@lazd que responde à pergunta feita. Então, por que o voto negativo? Ou você apenas comentou segundos após o voto negativo na resposta de um ano?
Randy Hall

A solução usa não matchesSelectorprefixado (que nem funciona na versão mais recente do Chrome), polui o namespace global (ret não foi declarado), não retorna um NodeList como o querySelectorMethods deve. Não acho que seja uma boa solução, daí o downvote.
tarde

@lazd - A questão original usa uma função sem prefixo e um namespace global. É uma solução perfeitamente adequada para a questão apresentada. Se você não acha que é uma boa solução, não use, nem sugira uma edição. Se for funcionalmente incorreto ou spam, considere um downvote.
Randy Hall

2

A solução a seguir é diferente das propostas até agora e funciona para mim.

A lógica é que você selecione primeiro todos os filhos correspondentes e, em seguida, filtre aqueles que não são filhos diretos. Um filho é um filho direto se não tiver um pai correspondente com o mesmo seletor.

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!


Gosto disso por sua brevidade e pela simplicidade da abordagem, embora provavelmente não tenha um bom desempenho se chamado em alta frequência com árvores grandes e seletores excessivamente gananciosos.
brennanyoung

1

Criei uma função para lidar com essa situação, pensei em compartilhá-la.

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

Em essência, o que você está fazendo é gerar uma string aleatória (a função randomString aqui é um módulo npm importado, mas você pode fazer o seu próprio.) E usar essa string aleatória para garantir que você obtenha o elemento que está esperando no seletor. Então você está livre para usar o >depois disso.

O motivo pelo qual não estou usando o atributo id é que o atributo id já pode ser usado e não quero substituí-lo.


0

Bem, podemos facilmente obter todos os filhos diretos de um elemento usando childNodese podemos selecionar ancestrais com uma classe específica com querySelectorAll, portanto, não é difícil imaginar que poderíamos criar uma nova função que obtenha ambos e compare os dois.

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: Isso retornará um Array of Nodes, não um NodeList.

Uso

 document.getElementById("myDiv").queryDirectChildren(".foo");

0

Eu gostaria de acrescentar que você pode estender a compatibilidade de : scope apenas atribuindo um atributo temporário ao nó atual.

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");

-1

Eu teria ido com

var myFoo = document.querySelectorAll("#myDiv > .foo");
var myDiv = myFoo.parentNode;

-2

Estou apenas fazendo isso, mesmo sem tentar. Isso funcionaria?

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

Experimente, talvez funcione, talvez não. Apolovies, mas não estou no computador agora para experimentar (respondendo do meu iPhone).


3
O escopo do QSA é o elemento que está sendo chamado, então ele procuraria outro elemento dentro de myDiv com o mesmo ID de myDiv e, em seguida, seus filhos com .foo. Você poderia fazer algo como `document.querySelectorAll ('#' + myDiv.id + '> .foo');
provavelmente até
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.