Programação Por Equipe DevBras

Capa do artigo: Seletor que não quebra quando o layout muda: data-atributo e closest

Seletor que não quebra quando o layout muda: data-atributo e closest

Trocar parent().parent().next() por um atributo estável e por closest dentro do componente faz o script sobreviver à próxima mudança de layout.

O botão "+" do carrinho aumentava a quantidade com $(this).parent().prev().find('input'). Funcionou por um ano. Aí o designer envolveu o campo e o botão numa <div> nova para alinhar os dois, e o "+" passou a aumentar a quantidade do produto de cima. Ninguém mexeu no JavaScript; o JavaScript é que dependia do desenho.

A resposta curta: marque o que o script precisa com um atributo seu, estável (data-*), suba até a raiz do componente com closest e busque dentro dela com querySelector. Assim o script descreve "o campo de quantidade deste item", não "o input dentro do irmão anterior do pai". Mudanças de layout passam a não importar.

Por que o caminho estrutural quebra

Navegar pelo DOM com parent, next, prev, children e eq é descrever a posição do elemento na árvore. Essa posição é responsabilidade de quem desenha a tela: uma <div> a mais para alinhar, a troca da ordem de dois blocos ou um ícone novo antes do texto mudam a árvore sem mudar nada do que a tela faz.

Seletores longos têm o mesmo defeito. .produto > div:nth-child(2) > span está amarrado à ordem dos filhos. E seletores por classe visual (.btn-azul, .col-md-4) estão amarrados à aparência: a classe muda quando a cor muda, e o script para de achar o elemento por um motivo que não tem nada a ver com ele.

O pior é que esse tipo de quebra raramente dá erro. Quando o caminho acha o elemento errado, o script segue feliz e altera o produto de cima. Quando não acha nada, o jQuery devolve uma coleção vazia e silenciosamente não faz nada. Você descobre pelo cliente.

Como deixar o seletor estável

  1. Marque a raiz de cada componente com um atributo próprio, como data-item-carrinho.
  2. Marque cada parte que o script lê ou altera: data-qtd, data-subtotal, data-acao="aumentar".
  3. No evento, parta do elemento clicado e suba até a raiz com closest.
  4. De dentro da raiz, busque as partes com querySelector. Nunca no documento inteiro quando há vários itens iguais.
  5. Se a parte esperada não existir, avise no console e saia. Não deixe o erro estourar para a pessoa.
  6. Deixe as classes para o CSS. O script não lê classe visual.

O item do carrinho marcado

<ul data-carrinho>
  <li data-item-carrinho data-produto-id="42" data-preco="199.90">
    <span class="titulo-produto">Tênis Corrida</span>
    <div class="linha-flex">
      <label>Quantidade <input type="number" name="qtd[42]" value="1" min="1" data-qtd></label>
      <button type="button" data-acao="aumentar" aria-label="Aumentar quantidade de Tênis Corrida">+</button>
    </div>
    <strong data-subtotal>R$ 199,90</strong>
  </li>
</ul>

As classes titulo-produto e linha-flex continuam existindo, mas são do CSS. O designer pode renomear, envolver em mais <div> ou trocar a ordem; enquanto os atributos data-* estiverem dentro do <li>, o script funciona.

O script que sobe até o componente

const formatoReal = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' });

document.querySelector('[data-carrinho]').addEventListener('click', (evento) => {
  const botao = evento.target.closest('[data-acao="aumentar"]');
  if (!botao) return;

  const item = botao.closest('[data-item-carrinho]');
  const qtd = item?.querySelector('[data-qtd]');
  const subtotal = item?.querySelector('[data-subtotal]');

  if (!qtd || !subtotal) {
    console.warn('item do carrinho sem quantidade ou subtotal', item);
    return;
  }

  qtd.value = Number(qtd.value) + 1;
  subtotal.textContent = formatoReal.format(Number(item.dataset.preco) * Number(qtd.value));
});

Um listener só, no <ul>, atende todos os itens, inclusive os que forem adicionados depois. O closest sobe a partir do botão até achar o <li> marcado, não importa quantas camadas existam no caminho. E o querySelector busca dentro do item, então nunca pega o campo do vizinho.

No jQuery, a mesma ideia é $(this).closest('[data-item-carrinho]').find('[data-qtd]'). O que importa não é a biblioteca, é trocar a descrição de posição por uma descrição de papel. Se você ainda está decidindo como amarrar o evento a vários botões, o post saber qual elemento foi clicado sem um id em cada botão aprofunda a delegação.

Cálculo com Number e ponto flutuante serve para exibir um subtotal provisório. O valor que vale é o do servidor, de preferência em centavos inteiros, como em dinheiro em centavos.

Escolhas que ajudam no dia a dia

Atributo de comportamento, não de estilo

Um prefixo claro deixa o contrato visível: quem vê data-acao ou data-qtd no HTML sabe que algum script depende daquilo e não remove por engano. Algumas equipes preferem classes com prefixo js-, com o mesmo efeito. Escolha um padrão e use no projeto inteiro.

Id serve para o que é único na página

O cabeçalho, o formulário de busca, o modal de login: esses podem ser achados por id. Itens de lista não, porque se repetem. Gerar id="qtd-42" para cada item e montar o seletor concatenando o número funciona, mas é mais frágil que buscar dentro do componente.

Testes usam o mesmo raciocínio

Testes de interface que clicam em div:nth-child(3) > button quebram junto com o layout. Ferramentas como Playwright e Testing Library incentivam buscar pelo papel e pelo nome acessível (por exemplo, o botão chamado "Aumentar quantidade de Tênis Corrida"), e oferecem busca por um atributo de teste quando não há nome. Se o teste acha o botão pelo nome, o leitor de tela também acha.

Erros comuns

  • Encadear parent(), next() e prev(). Uma <div> a mais no layout e o script altera o elemento errado, sem nenhum erro.
  • Buscar no documento inteiro. Com dez itens na lista, document.querySelector('[data-qtd]') sempre devolve o primeiro.
  • Usar classe visual como seletor. A troca de cor ou de framework CSS desliga o comportamento.
  • Usar nth-child e eq para achar uma parte. Reordenar os blocos muda o que o script lê.
  • Ignorar o "não achou". O jQuery engole a coleção vazia e o bug só aparece quando o cliente reclama.
  • Listener em cada item. Itens adicionados depois ficam sem comportamento e a memória cresce à toa.

Checklist

  • Cada componente com comportamento tem um atributo de raiz.
  • Cada parte lida ou alterada por script tem um atributo próprio.
  • Nenhum seletor de script usa classe visual, nth-child ou cadeia de parent.
  • Toda busca de parte é feita a partir da raiz do componente.
  • O caso "não achou" registra um aviso no console de desenvolvimento.
  • Listas usam um listener delegado no contêiner.
  • Os testes acham os elementos por papel e nome, ou por atributo de teste.

Pegue o script de interface mais antigo do seu projeto e procure por parent(), next() e nth-child. Se alguém trocar a ordem de dois blocos do layout amanhã, ele ainda acha o elemento certo, ou está descrevendo o desenho em vez do componente?

Avalie este artigo

Referência: http://wbruno.com.br/jquery/navegando-no-dom-jquery/