Qual botão foi clicado: delegação de evento com closest e data-id
Troque um ouvinte e um id por botão por um ouvinte só no contêiner, ache o botão certo com closest mesmo quando o clique cai no ícone e leve o identificador num data-id.
O carrinho tem trinta itens, cada um com um botão de remover que ganhou um id numerado, remover-1 até remover-30, e uma função própria. O cliente adiciona o trigésimo primeiro item sem recarregar a página e o botão dele não faz nada. Às vezes, nos outros, também não: o clique no ícone da lixeira chega com id vazio. A resposta curta: coloque um único ouvinte no contêiner da lista, use event.target.closest() para achar o botão e leia o identificador do item num atributo data-id. Item novo funciona sem nenhum código extra, e o ícone deixa de ser problema.
O exemplo deste guia é esse carrinho, com botões de remover e de aumentar a quantidade, primeiro em JavaScript puro e depois em jQuery.
Por que o id em cada botão quebra
Quando você liga um ouvinte em cada botão no carregamento da página, só os botões que existem naquele momento recebem o ouvinte. O item que entra depois, por AJAX, nasce sem nenhum. Você pode lembrar de ligar o ouvinte toda vez que insere um item, mas aí basta esquecer em um lugar.
O segundo problema é o alvo. Um clique no navegador acontece no elemento mais interno sob o ponteiro e depois sobe pelos ancestrais: do ícone para o botão, do botão para o item da lista, da lista para o resto da página. Esse caminho se chama propagação, e é ele que permite escutar no pai. Mas é também por causa dele que event.target pode ser o desenho da lixeira dentro do botão, e não o botão. Quem lê event.target.id recebe uma string vazia sempre que o clique cai no ícone, e "às vezes funciona".
O terceiro é o próprio id. Id precisa ser único na página. Quando o mesmo componente é renderizado duas vezes, por exemplo no carrinho lateral e no resumo, os ids se repetem, e getElementById devolve sempre o primeiro. O id do HTML serve para ligar rótulo, foco e âncora. Não é chave de banco.
Como fazer com um ouvinte só
- Use elementos
buttoncomtype="button". Dentro de um formulário, botão sem tipo envia o formulário. - Coloque a ação e o identificador do registro em atributos
data-:data-acao="remover"edata-id="42". - Ligue um único ouvinte de clique no contêiner que existe desde o carregamento, a lista do carrinho.
- No ouvinte, suba do alvo até o botão com
closest('button[data-acao]'). - Se não achou botão, ou achou um fora da lista, ignore o clique.
- Leia
dataset.acaoedataset.ide chame a função certa. - No servidor, confira que o item pertence ao carrinho de quem pediu.
O HTML de um item
<ul id="carrinho">
<li>
<span>Caneca azul</span>
<span>Quantidade: 1</span>
<button type="button" data-acao="aumentar" data-id="42"
aria-label="Aumentar quantidade de Caneca azul">+</button>
<button type="button" data-acao="remover" data-id="42"
aria-label="Remover Caneca azul">
<svg aria-hidden="true" width="16" height="16"><use href="#icone-lixeira"></use></svg>
</button>
</li>
</ul>
O aria-label dá nome ao botão que só tem ícone e diz de qual item ele é. Sem isso, o leitor de tela anuncia trinta botões chamados "botão". O aria-hidden no desenho evita que ele seja lido como conteúdo.
O ouvinte em JavaScript puro
const carrinho = document.getElementById('carrinho');
carrinho.addEventListener('click', function (evento) {
const botao = evento.target.closest('button[data-acao]');
if (!botao || !carrinho.contains(botao)) return;
const id = botao.dataset.id;
if (botao.dataset.acao === 'remover') {
removerItem(id, botao.closest('li'));
} else if (botao.dataset.acao === 'aumentar') {
aumentarQuantidade(id);
}
});
async function removerItem(id, linha) {
const resposta = await fetch('/carrinho/itens/' + encodeURIComponent(id), { method: 'DELETE' });
if (resposta.ok) {
linha.remove();
}
}
Siga o clique no ícone. evento.target é o elemento use dentro do svg. O closest sobe a árvore a partir dele e para no primeiro ancestral que casa com o seletor, o botão de remover. evento.currentTarget, por sua vez, seria a lista inteira, porque é nela que o ouvinte está; ela não diz qual item foi clicado. O teste com contains é uma proteção para quando o contêiner está dentro de outro que também usa data-acao: o closest pode subir além da lista e achar um botão de fora.
O item 31, inserido depois, já funciona. Ele não precisa de ouvinte próprio, porque o clique dele sobe até a mesma lista.
O mesmo em jQuery
$('#carrinho').on('click', 'button[data-acao]', function () {
const botao = $(this);
const id = botao.data('id');
if (botao.data('acao') === 'remover') {
removerItem(id, botao.closest('li')[0]);
}
});
Com o seletor como segundo argumento do on, o jQuery faz a delegação e entrega em this o botão que casou com o seletor, não o ícone. Um cuidado: .data('id') converte "42" no número 42, enquanto dataset.id devolve sempre texto. Se o identificador tem zeros à esquerda, como um código de produto "00042", leia com .attr('data-id').
Teclado e permissão
Um button de verdade dispara o mesmo evento de clique quando recebe Enter ou Espaço com foco. Um div com ouvinte de clique, não: ele nem recebe foco pelo Tab. Por isso a delegação funciona para teclado sem nenhuma linha extra, desde que o alvo seja botão. A diferença entre os eventos de ponteiro e o clique está em evento de mouse não cobre o teclado.
E o data-id está no HTML, então qualquer pessoa pode editar o valor nas ferramentas do navegador e pedir para remover o item 43, que é de outro cliente. O servidor precisa conferir que o item pertence ao carrinho da sessão antes de apagar. É a mesma regra de qualquer identificador que vem do navegador.
Erros comuns
- Ler
event.target.iddireto. O clique no ícone traz o id vazio e a ação falha de forma intermitente. - Ligar ouvinte em cada botão no carregamento. Itens inseridos depois ficam sem ação.
- Usar o id do HTML como chave do registro. Componente renderizado duas vezes repete o id e
getElementByIdsempre acha o primeiro. - Botão sem
typedentro de formulário. O clique em remover envia o formulário e recarrega a página. stopPropagationnum filho. O clique não sobe e a delegação no pai nunca fica sabendo.- Confiar no
data-idno servidor. Qualquer pessoa troca o número e mexe no carrinho de outra.
Checklist
- A lista tem um único ouvinte de clique.
- O botão é achado com
closest, não comevent.targetdireto. - O identificador do registro está em
data-id, não no id do HTML. - Todos os controles são
buttoncomtype="button". - Botões só com ícone têm
aria-label. - Um item inserido sem recarregar responde ao clique.
- Enter e Espaço acionam a mesma ação que o clique.
- O servidor confere o dono do registro antes de agir.
Adicione um item novo na sua lista sem recarregar a página e clique exatamente no ícone do botão dele. A ação encontrou o registro certo? E esse controle é um botão de verdade, que você alcança com Tab?
Referência: http://forum.imasters.com.br/topic/380561-resolvidopegar-o-id-do-elemento-clicado-com-jquery/