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
- Marque a raiz de cada componente com um atributo próprio, como
data-item-carrinho. - Marque cada parte que o script lê ou altera:
data-qtd,data-subtotal,data-acao="aumentar". - No evento, parta do elemento clicado e suba até a raiz com
closest. - De dentro da raiz, busque as partes com
querySelector. Nunca no documento inteiro quando há vários itens iguais. - Se a parte esperada não existir, avise no console e saia. Não deixe o erro estourar para a pessoa.
- 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()eprev(). 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-childeeqpara 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-childou cadeia deparent. - 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?
Referência: http://wbruno.com.br/jquery/navegando-no-dom-jquery/