How to remove an eventListener properly?

Benjamin Réthoré

I'm having fun with a vanilla JS Tic tac toe Every cell is a button. To handle clicks I'm adding an eventListener to a wrapping div:

board.addEventListener("click", handleClick, true);

And later in the printWinner() function I try to remove it with no effect...

const printWinner = (winner) => {
    // ...
    board.removeEventListener("click", handleClick);
}

How do I remove this eventListener?

Complete code:

// Some settings
const symbols = ['X', '0'];
const board   = document.querySelector('#board');

// Append a result popup
const results = document.createElement("div");
results.classList.add("message");
document.body.insertBefore(results, board);

// Winning combinations
const combinations = [
  [1,2,3],
  [4,5,6],
  [7,8,9],
  [1,4,7],
  [2,5,8],
  [3,6,9],
  [1,5,9],
  [7,5,3]
]

// Base variables
let i = 1;
let move= 0;

// Check if there's a winner
const checkWin = () => {
  let winner = false;

  combinations.forEach(combination => {
    let c0 = board.querySelector(`#c${combination[0]}`).innerHTML || undefined;
    let c1 = board.querySelector(`#c${combination[1]}`).innerHTML || undefined;
    let c2 = board.querySelector(`#c${combination[2]}`).innerHTML || undefined;

    if (c0 === c1 && c0 === c2 && c0 !== undefined) {
      winner = symbols.indexOf(c0) + 1;
      printWinner(winner);
      return;
    }
  });
}

// Cells click handling
const handleClick = (event) => {
  if(!event.target.innerHTML && event.target.nodeName == 'BUTTON') {
    let currSymbol = move % 2 ? symbols[1] : symbols[0];
    event.target.innerHTML = currSymbol;
    event.target.setAttribute("disabled", true);
    event.preventDefault();
    checkWin();
    move++;
  }
}

// Bind the click handler
board.addEventListener("click", handleClick, true);

// Print the winner
const printWinner = (winner) => {
  let winnerMessage = document.createTextNode(`Player ${winner} wins!`);
  results.appendChild(winnerMessage);
  results.classList.add("is-visible");
  setTimeout(() => {
    results.classList.remove("is-visible");
  }, 4000)
  board.removeEventListener("click", handleClick);
}

// Fill the board
(function fillDom() {
  let dom = '';
  for (let row = 1; row <= 3; row++) {
    dom += '<div class="board__row">';

    for (let cell = 1; cell <= 3; cell++) {
      dom += `<button id="c${i}" class="board__cell"></button>`;

      if (cell == 3) {
        dom += `</div>`;
      }
      i++;
    }
  }
  board.innerHTML = dom;
})();
:root {
  --bgColor: #fff;
  --mainColor: #04e;
  --mainColor-hover: #dde7ff;
  --mainColor-active: #eef3ff;
  --messageColor: #04e;
}
html {
  box-sizing: border-box;
}
*,
*:before,
*:after {
  box-sizing: inherit;
  padding: 0;
  margin: 0;
}
body {
  background-color: var(--bgColor);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
  font-size: 18px;
  line-height: 1.48;
}
.board {
  width: 300px;
  height: 300px;
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
}
.board__row {
  width: 100%;
  border-bottom: 2px solid var(--mainColor);
  display: flex
}
.board__row:last-of-type {
  border-bottom: 0;
}
.board__cell {
  flex: 1 1 auto;
  width: 100px;
  height: 100px;
  text-align: center;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  border: 0;
  border-radius: 0;
  border-right: 2px solid var(--mainColor);
  font-size: 24px;
  font-weight: 600;
  outline: 0;
  cursor: pointer;
  color: var(--mainColor);
  background-color: var(--bgColor);
  transition: background-color 160ms cubic-bezier(0.455, 0.03, 0.515, 0.955);
}
.board__cell:hover {
  background-color: var(--mainColor-hover);
}
.board__cell[disabled] {
  background-color: var(--mainColor-active);
}
.board__row .board__cell:last-child {
  border-right: 0;
}
.message {
  position: fixed;
  z-index: 1;
  top: 0;
  left: 50%;
  opacity: 0;
  transform: translate(-50%, -100px);
  transition: all 400ms cubic-bezier(0.6, -0.28, 0.735, 0.045);
  color: #fff;
  background-color: var(--messageColor);
  border-radius: 4px;
  padding: 12px 24px;
  box-shadow: 0 2px 8px rgba(0,0,0,.16)
}
.message.is-visible {
  opacity: 1;
  transform: translate( -50%, 24px);
  transition-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
<div id="board" class="board"></div>

Or see it on codepen.

Mike Cluck

The problem is how you're using the "capture" flag.

When you setup the event listener, you're registering it as a capturing listener:

board.addEventListener("click", handleClick, true);
//                                       -----^

But when you're attempting to remove it, you are not specifying a capturing listener:

// Notice the lack of a third argument
board.removeEventListener("click", handleClick);

To quote the MDN page:

Removal of a capturing listener does not affect a non-capturing version of the same listener, and vice versa.

So what you need to do is specify that you want to remove a capturing listener:

board.removeEventListener("click", handleClick, true);

Your code with this change in place:

// Some settings
const symbols = ['X', '0'];
const board   = document.querySelector('#board');

// Append a result popup
const results = document.createElement("div");
results.classList.add("message");
document.body.insertBefore(results, board);

// Winning combinations
const combinations = [
  [1,2,3],
  [4,5,6],
  [7,8,9],
  [1,4,7],
  [2,5,8],
  [3,6,9],
  [1,5,9],
  [7,5,3]
]

// Base variables
let i = 1;
let move= 0;

// Check if there's a winner
const checkWin = () => {
  let winner = false;

  combinations.forEach(combination => {
    let c0 = board.querySelector(`#c${combination[0]}`).innerHTML || undefined;
    let c1 = board.querySelector(`#c${combination[1]}`).innerHTML || undefined;
    let c2 = board.querySelector(`#c${combination[2]}`).innerHTML || undefined;

    if (c0 === c1 && c0 === c2 && c0 !== undefined) {
      winner = symbols.indexOf(c0) + 1;
      printWinner(winner);
      return;
    }
  });
}

// Cells click handling
const handleClick = (event) => {
  if(!event.target.innerHTML && event.target.nodeName == 'BUTTON') {
    let currSymbol = move % 2 ? symbols[1] : symbols[0];
    event.target.innerHTML = currSymbol;
    event.target.setAttribute("disabled", true);
    event.preventDefault();
    checkWin();
    move++;
  }
}

// Bind the click handler
board.addEventListener("click", handleClick, true);

// Print the winner
const printWinner = (winner) => {
  let winnerMessage = document.createTextNode(`Player ${winner} wins!`);
  results.appendChild(winnerMessage);
  results.classList.add("is-visible");
  setTimeout(() => {
    results.classList.remove("is-visible");
  }, 4000)
  board.removeEventListener("click", handleClick, true);
}

// Fill the board
(function fillDom() {
  let dom = '';
  for (let row = 1; row <= 3; row++) {
    dom += '<div class="board__row">';

    for (let cell = 1; cell <= 3; cell++) {
      dom += `<button id="c${i}" class="board__cell"></button>`;

      if (cell == 3) {
        dom += `</div>`;
      }
      i++;
    }
  }
  board.innerHTML = dom;
})();
:root {
  --bgColor: #fff;
  --mainColor: #04e;
  --mainColor-hover: #dde7ff;
  --mainColor-active: #eef3ff;
  --messageColor: #04e;
}
html {
  box-sizing: border-box;
}
*,
*:before,
*:after {
  box-sizing: inherit;
  padding: 0;
  margin: 0;
}
body {
  background-color: var(--bgColor);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
  font-size: 18px;
  line-height: 1.48;
}
.board {
  width: 300px;
  height: 300px;
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
}
.board__row {
  width: 100%;
  border-bottom: 2px solid var(--mainColor);
  display: flex
}
.board__row:last-of-type {
  border-bottom: 0;
}
.board__cell {
  flex: 1 1 auto;
  width: 100px;
  height: 100px;
  text-align: center;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  border: 0;
  border-radius: 0;
  border-right: 2px solid var(--mainColor);
  font-size: 24px;
  font-weight: 600;
  outline: 0;
  cursor: pointer;
  color: var(--mainColor);
  background-color: var(--bgColor);
  transition: background-color 160ms cubic-bezier(0.455, 0.03, 0.515, 0.955);
}
.board__cell:hover {
  background-color: var(--mainColor-hover);
}
.board__cell[disabled] {
  background-color: var(--mainColor-active);
}
.board__row .board__cell:last-child {
  border-right: 0;
}
.message {
  position: fixed;
  z-index: 1;
  top: 0;
  left: 50%;
  opacity: 0;
  transform: translate(-50%, -100px);
  transition: all 400ms cubic-bezier(0.6, -0.28, 0.735, 0.045);
  color: #fff;
  background-color: var(--messageColor);
  border-radius: 4px;
  padding: 12px 24px;
  box-shadow: 0 2px 8px rgba(0,0,0,.16)
}
.message.is-visible {
  opacity: 1;
  transform: translate( -50%, 24px);
  transition-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
<div id="board" class="board"></div>

Collected from the Internet

Please contact [email protected] to delete if infringement.

edited at
0

Comments

0 comments
Login to comment

Related