0

I am practicing html/vanilla JS by building a tic tac toe app. I add an event listener to the board square <div>s, and create elements with X and O and insert them into the DOM, then call a function to check if there is a winner. The checkIfPlayerWins() function does some checking and then calls and alert if the players wins or there is a draw.

The problem I am running into is that if a player wins, the alert is being called before the X or O is inserted into the DOM which just looks funny. I would like to not use a setTimeout as that just seems a bit hacky. I tried async/await and also creating a new Promise from it but can't seem to get it to work.

Okay here is some code and relevant links: insertAdjacentElement MDN docs My github/ code repo

  setEventListeners() {
    const board = document.querySelector('.board')
    const squares = board.children

    for (let i = 0; i < squares.length; i++) {
      squares[i].addEventListener('click', (event) => {
        const squareEl = event.target
        if (!squareEl.firstChild) {
          this.makeMove(squareEl)
        } else {
          alert('That square is already taken!')
        }
      })
    }
  }

  makeMove(eventTarget) {
    let playerMarker = ''
    if (this.playerOneTurn) {
      playerMarker = 'X'
    } else {
      playerMarker = 'O'
    }

    eventTarget.insertAdjacentElement('beforeend', createElementFromHTML(`<div>${playerMarker}</div>`))

    this.updateGameState(eventTarget.className, playerMarker)

    this.playerOneTurn = !this.playerOneTurn
    this.updatePlayerTurnText()
  }

export function createElementFromHTML(htmlString) {
  const template = document.createElement('template')
  template.innerHTML = htmlString.trim()
  return template.content.firstChild
}

  updateGameState(squareName, playerMarker) {
    this.gameState[squareName] = playerMarker
    this.checkIfPlayerWins()
  }

  checkIfPlayerWins() {
    // TODO make this smarter
    const winningIndexes = [
      [0, 1, 2],
      [3, 4, 5],
      [6, 7, 8],
      [0, 3, 6],
      [1, 4, 7],
      [2, 5, 8],
      [0, 4, 8],
      [2, 4, 6],
    ]

    let winner = null

    winningIndexes.forEach((combos) => {
      if (combos.every((combo) => this.gameState[`square-${combo + 1}`] === 'X')) {
        winner = this.playerOne
      } else if (combos.every((combo) => this.gameState[`square-${combo + 1}`] === 'O')) {
        winner = this.playerTwo
      }
    })

    if (Object.values(this.gameState).every((square) => square != '')) {
      alert('Game is a draw!')
    } else if (winner) {
      alert(`${winner} wins!!`)
    }
  }

These are all the relevant functions, and if you see in the makeMove() function, I call eventTarget.insertAdjacentElement, but then I call updatePlayerTurn() which calls checkIfPlayerWins() which calls the alert(). This alert() in the checkIfPlayerWins() function is executing before the insertAdjacentElement element gets inserted into the DOM. Can anyone help me figure out how I can wait for the DOM changes to happen before the alert() gets called? Thanks!

Michael Mudge
  • 369
  • 1
  • 5
  • 10
  • `alert` blocks the UI from updating. You can use `requestAnimationFrame` if `setTimeout(fn, 0)` seems too "hacky". – Heretic Monkey Jan 27 '21 at 02:43
  • shouldn't the DOM have already updated though? `alert` stops previous code? Any relevant links for me to learn more about this? – Michael Mudge Jan 27 '21 at 02:44
  • 1
    There are a number of links in the answers to the question linked in my second comment. https://developers.google.com/web/fundamentals/performance/rendering/ comes right from a browser vendor... – Heretic Monkey Jan 27 '21 at 02:47
  • Great links, thanks! So all the JS will run before a new paint of the DOM, so my code is hanging on the `alert` before the element is rendered to the DOM and so making the alerts asynchronous makes the code not hang on it. Cool, okay so I guess using `setTimeout` on the alert isn't as hacky as I previously thought. Thanks! – Michael Mudge Jan 27 '21 at 03:07

0 Answers0