Nesta aula, vamos explorar tópicos avançados sobre eventos em JavaScript, fundamentais para criar aplicações web interativas e eficientes. Entender como os eventos se propagam pelo DOM, como delegar eventos para elementos pai e como interromper comportamentos padrão ou a propagação é essencial para evitar bugs e melhorar a performance.

Vamos começar com a propagação de eventos, um conceito que descreve a ordem em que os elementos recebem um evento quando ele ocorre em um elemento aninhado. Em seguida, veremos a delegação de eventos, uma técnica poderosa para lidar com eventos em muitos elementos de forma eficiente. Por fim, aprenderemos a usar preventDefault para cancelar ações padrão do navegador e stopPropagation para interromper a propagação do evento.

Propagação (bubbling/capturing)

Quando um evento ocorre em um elemento, ele não é disparado apenas naquele elemento, mas sim em todos os seus ancestrais. Esse processo é chamado de propagação de eventos. Existem três fases: a fase de captura (capturing), a fase alvo (target) e a fase de bolha (bubbling). Na fase de captura, o evento desce do elemento raiz até o alvo. Na fase alvo, o evento atinge o elemento que disparou o evento. Na fase de bolha, o evento sobe do alvo de volta ao elemento raiz.

Por padrão, os ouvintes de eventos são acionados na fase de bolha. Para usar a fase de captura, você pode passar true como terceiro parâmetro para addEventListener. Entender a propagação é crucial para evitar comportamentos inesperados e para implementar padrões como delegação de eventos.

// Exemplo de bubbling: clique no botão dispara eventos no botão, no div e no body
document.querySelector('button').addEventListener('click', () => console.log('Botão'));
document.querySelector('div').addEventListener('click', () => console.log('Div'));
document.body.addEventListener('click', () => console.log('Body'));
// Ao clicar no botão, a ordem será: Botão, Div, Body

// Exemplo de capturing: usando true
document.querySelector('div').addEventListener('click', () => console.log('Div capturing'), true);
document.body.addEventListener('click', () => console.log('Body capturing'), true);
// Ao clicar no botão, a ordem será: Body capturing, Div capturing, Botão

Delegação de eventos

A delegação de eventos é uma técnica que aproveita a propagação de eventos para gerenciar eventos de forma eficiente. Em vez de adicionar um ouvinte a cada elemento filho, você adiciona um único ouvinte a um elemento pai e usa a propriedade event.target para identificar qual filho foi clicado. Isso é especialmente útil quando você tem muitos elementos ou elementos que são adicionados dinamicamente.

Por exemplo, em uma lista com muitos itens, adicionar um ouvinte a cada li seria ineficiente. Com delegação, adicionamos um ouvinte ao ul e verificamos se o clique ocorreu em um li. Isso reduz o número de ouvintes e simplifica o código.

// Delegação de eventos: clique em qualquer li da lista
document.querySelector('ul').addEventListener('click', function(event) {
  if (event.target.tagName === 'LI') {
    console.log('Clicou no item:', event.target.textContent);
  }
});

// Adicionando um novo item dinamicamente
const novoItem = document.createElement('li');
novoItem.textContent = 'Item novo';
document.querySelector('ul').appendChild(novoItem);
// O clique no novo item também será capturado sem precisar adicionar um novo ouvinte

preventDefault e stopPropagation

preventDefault() é um método do objeto Event que impede a ação padrão do navegador associada ao evento. Por exemplo, clicar em um link (<a>) normalmente navega para uma URL, mas com preventDefault() podemos cancelar essa navegação. É comumente usado em formulários para evitar o envio padrão e validar os dados antes.

stopPropagation() interrompe a propagação do evento nas fases de captura ou bolha. Isso impede que os ouvintes de eventos em elementos pai sejam acionados. É útil quando você quer que apenas o elemento alvo lide com o evento, sem afetar os ancestrais.

// preventDefault: cancelar o envio de um formulário
document.querySelector('form').addEventListener('submit', function(event) {
  event.preventDefault();
  console.log('Formulário não será enviado');
});

// stopPropagation: impedir que o clique em um botão afete o div pai
document.querySelector('button').addEventListener('click', function(event) {
  event.stopPropagation();
  console.log('Clique no botão, mas não propaga');
});
document.querySelector('div').addEventListener('click', function() {
  console.log('Div não será acionado');
});

Boas práticas

Ao trabalhar com eventos avançados, lembre-se de: (1) usar delegação sempre que possível para melhorar performance e simplificar o código; (2) evitar o uso excessivo de stopPropagation(), pois pode quebrar funcionalidades esperadas em outros componentes; (3) sempre considerar a propagação ao depurar eventos; e (4) usar preventDefault() apenas quando necessário, para não prejudicar a acessibilidade.

Referências

Exercícios

  1. Crie um layout com três divs aninhadas (externa, média e interna). Adicione um ouvinte de clique a cada uma. Ao clicar na div interna, mostre no console a ordem dos eventos (bubbling).

    ✓ Resposta:
    <div id="externa">
      <div id="media">
        <div id="interna">Clique aqui</div>
      </div>
    </div>
    <script>
      document.getElementById('externa').addEventListener('click', () => console.log('Externa'));
      document.getElementById('media').addEventListener('click', () => console.log('Média'));
      document.getElementById('interna').addEventListener('click', () => console.log('Interna'));
      // Ao clicar na interna, a ordem será: Interna, Média, Externa (bubbling)
    </script>
  2. Usando delegação, crie uma lista não ordenada com 5 itens e um ouvinte no ul que exibe o texto do item clicado.

    ✓ Resposta:
    <ul id="lista">
      <li>Item 1</li>
      <li>Item 2</li>
      <li>Item 3</li>
      <li>Item 4</li>
      <li>Item 5</li>
    </ul>
    <script>
      document.getElementById('lista').addEventListener('click', function(event) {
        if (event.target.tagName === 'LI') {
          console.log(event.target.textContent);
        }
      });
    </script>
  3. Faça um formulário com um campo de texto e um botão de envio. Use preventDefault para evitar o envio e exiba uma mensagem de validação se o campo estiver vazio.

    ✓ Resposta:
    <form id="meuForm">
      <input type="text" id="nome" placeholder="Digite seu nome">
      <button type="submit">Enviar</button>
    </form>
    <script>
      document.getElementById('meuForm').addEventListener('submit', function(event) {
        event.preventDefault();
        const nome = document.getElementById('nome').value;
        if (nome.trim() === '') {
          alert('O campo nome é obrigatório.');
        } else {
          alert('Formulário válido!');
        }
      });
    </script>
  4. Crie um botão dentro de uma div. Adicione ouvintes de clique no botão e na div. No ouvinte do botão, use stopPropagation para impedir que o clique no botão dispare o ouvinte da div.

    ✓ Resposta:
    <div id="container">
      <button id="meuBotao">Clique</button>
    </div>
    <script>
      document.getElementById('meuBotao').addEventListener('click', function(event) {
        event.stopPropagation();
        console.log('Botão clicado');
      });
      document.getElementById('container').addEventListener('click', function() {
        console.log('Div clicada');
      });
      // Ao clicar no botão, apenas 'Botão clicado' aparece
    </script>
  5. Explique a diferença entre event.target e event.currentTarget em um evento delegado. Dê um exemplo.

    ✓ Resposta:
    // event.target é o elemento que disparou o evento (o alvo original).
    // event.currentTarget é o elemento que possui o ouvinte (o elemento pai na delegação).
    // Exemplo:
    <ul id="lista">
      <li>Item 1</li>
      <li>Item 2</li>
    </ul>
    <script>
      document.getElementById('lista').addEventListener('click', function(event) {
        console.log('target:', event.target.tagName); // LI
        console.log('currentTarget:', event.currentTarget.tagName); // UL
      });
    </script>