1

I am making a javascript variable that's supposed to store an Array (not Nodelist) via the spread syntax:

const $corners = [...document.getElementsByClassName('corner')];

and filtering out all items that don't say 'Empty':

const emptyCorners = $corners.filter(corner => corner.innerText === 'Empty');

I'm getting an obnoxious console error that says: Cannot read property 'innerText' of undefined

And of course I'm putting this through babel with the airbnb preset. What am I doing wrong? Thank you for your time. My full code

window.addEventListener('load', () => {
  // Determine whether you are going first
  const humanTurnFirst = Math.random() >= 0.5;
  /**
   * Get an array of the text content of each of the tic-tac-toe buttons
   * @returns {Array} Array of the text content of each square, from top-left to bottom-right.
  */

  const getLayout = () => {
    // Array of buttons ordered from top-left to bottom right
    const buttons = [
      document.getElementsByClassName('corner-top-left')[0],
      document.getElementsByClassName('edge-top')[0],
      document.getElementsByClassName('corner-top-right')[0],
      document.getElementsByClassName('edge-left')[0],
      document.getElementsByClassName('center-button')[0],
      document.getElementsByClassName('edge-right')[0],
      document.getElementsByClassName('corner-bottom-left')[0],
      document.getElementsByClassName('edge-bottom')[0],
      document.getElementsByClassName('corner-bottom-right')[0],
    ];
    const layout = [];
    buttons.forEach(button => layout.push(button.innerText));
    return layout;
  };
  /**
   * Make the computer play a square
   * @param {Node} button The square to play
   */

  const autoClick = (button) => {
    const $turn = document.getElementsByClassName('turn')[0];
    $turn.innerText = 'Not your turn yet...';
    const $allButtons = [...document.getElementsByClassName('button')];
    const $allDisableableButtons = $allButtons
      .filter(
        element => element !== button
        && !element.disabled,
      );
    $allDisableableButtons.forEach((disableableButton) => {
      const thisButton = disableableButton;
      thisButton.disabled = true;
    });
    button.focus();
    setTimeout(() => {
      button.click();
      $allDisableableButtons.forEach((disableableButton) => {
        const thisButton = disableableButton;
        thisButton.disabled = false;
        $turn.innerText = 'Try clicking an empty space.';
      });
    }, 500);
  };
  /**
   * Calculate the best square for the computer to play.
   * @param {Array.<Node>} layout Array of the text of each square, from top-left to bottom right.
   * @param {Node|Boolean} previous The last move that you've made.
   */
  const computerTurn = (layout, previous, localHumanTurnFirst) => {
    const buttons = [
      document.getElementsByClassName('corner-top-left')[0],
      document.getElementsByClassName('edge-top')[0],
      document.getElementsByClassName('corner-top-right')[0],
      document.getElementsByClassName('edge-left')[0],
      document.getElementsByClassName('center-button')[0],
      document.getElementsByClassName('edge-right')[0],
      document.getElementsByClassName('corner-bottom-left')[0],
      document.getElementsByClassName('edge-bottom')[0],
      document.getElementsByClassName('corner-bottom-right')[0],
    ];
    const $corners = [...document.getElementsByClassName('corner')];
    // If there is no previous move, the computer goes first with a random corner.
    if (!previous) {
      const randomBelow4 = Math.floor(Math.random() * 4);
      const randomCorner = $corners[randomBelow4];
      autoClick(randomCorner);
      /* If the computer is going first,
        has already filled out a random corner,
        and there is nothing in the center,
        it will place another X in one of the adgacent corners.
      */
    } else if (!localHumanTurnFirst && layout.filter(element => element === 'X').length === 1 && previous !== buttons[4]) {
      const filledOutCorner = buttons.filter(element => element.innerText === 'X')[0];
      const diagonalCorner = document.getElementsByClassName(filledOutCorner.className
        .split(/\s+/)[2]
        .replace(/(left|right)/, match => (match === 'left' ? 'right' : 'left'))
        .replace(/(top|bottom)/, match => (match === 'top' ? 'bottom' : 'top')))[0];
      const emptyCorners = $corners.filter(corner => corner.innerText === 'Empty');
      const adjacentCorners = emptyCorners.filter(element => element !== diagonalCorner);
      const potentialCorners = adjacentCorners
        .filter(
          corner => !document.getElementsByClassName(`${corner.className.split(/\s+/)[2].split('-')[1]}-edge`)[0].innerText
            && !document.getElementsByClassName(`${corner.className.split(/\s+/)[2].split('-')[2]}-edge`)[0].innerText,
        );
      console.log(potentialCorners);
      /*       const randomPotentialCorner = adjacentCorners[Math.floor(Math.random())];
      autoClick(randomPotentialCorner);
    */ }
  };
  /**
   * Add event listener for squares
   * @param {Boolean} localHumanTurnFirst Whether you go first.
   */
  const squaresOnClick = (localHumanTurnFirst, isHumanTurn) => {
    const humanLetter = localHumanTurnFirst ? 'X' : 'O';
    const computerLetter = localHumanTurnFirst ? 'O' : 'X';
    const $squares = [...document.getElementsByClassName('button')];
    $squares.forEach((square) => {
      const thisSquare = square;
      square.addEventListener('click', () => {
        if (isHumanTurn) {
          thisSquare.innerText = humanLetter;
          computerTurn(getLayout(), thisSquare, localHumanTurnFirst);
          squaresOnClick(localHumanTurnFirst, false);
        } else {
          thisSquare.innerText = computerLetter;
          squaresOnClick(localHumanTurnFirst, true);
        }
        thisSquare.disabled = true;
      });
    });
  };
  /**
   * Turn the welcome screen into the game screen.
   * @param {Boolean} localHumanTurnFirst Whether you go first.
   */
  const spawnSquares = (localHumanTurnFirst) => {
    const $turn = document.getElementsByClassName('turn')[0];
    const $mainGame = document.getElementsByClassName('main-game')[0];
    $turn.innerText = 'Try clicking an empty space.';
    $mainGame.className = 'main-game dp-4 tic-tac-toe';
    $mainGame.setAttribute('aria-label', 'Tic-tac-toe grid');
    $mainGame.innerHTML = `
      <button class="button corner corner-top-left corner-top corner-left">Empty</button>
      <button class="button edge edge-top">Empty</button>
      <button class="button corner corner-top-right corner-top corner-right">Empty</button>
      <button class="button edge edge-left">Empty</button>
      <button class="button center-button">Empty</button>
      <button class="button edge edge-right">Empty</button>
      <button class="button corner corner-bottom-left corner-bottom corner-left">Empty</button>
      <button class="button edge edge-bottom">Empty</button>
      <button class="button corner corner-bottom-right corner-bottom corner-right">Empty</button>
    `;
    squaresOnClick(localHumanTurnFirst, localHumanTurnFirst);
    if (!localHumanTurnFirst) {
      computerTurn(getLayout(), false, localHumanTurnFirst);
    }
  };
  /**
   * Create the button that starts the game.
   */
  const welcomeButton = (localHumanTurnFirst) => {
    const $welcomeButton = document.getElementsByClassName('start-button')[0];
    $welcomeButton.addEventListener('click', () => spawnSquares(localHumanTurnFirst));
  };
  /**
   * Turn the main game into the welcome screen.
   * @param {Boolean} localHumanTurnFirst Whether you go first.
   */
  const welcome = (localHumanTurnFirst) => {
    const $mainGame = document.getElementsByClassName('main-game')[0];
    const $turn = document.getElementsByClassName('turn')[0];
    $turn.innerText = 'Welcome!';
    $mainGame.className = 'main-game dp-4 welcome center';
    $mainGame.innerHTML = `
    <section class="welcome-section">
      <h2 class="welcome-heading">Welcome to unbeatable tic-tac-toe!</h2>
      <p class="welcome-text">
        According to random chance, your turn has already been chosen
        as ${localHumanTurnFirst ? 'first (with an X)' : 'second (with an O)'}, which 
        means that the computer is going 
        ${localHumanTurnFirst ? 'second (with an O)' : 'first (with an X)'}. <strong>
          Press the start button to start the game!</strong
        >
      </p>
    </section>
    <button class="start-button button">Start</button>
  `;
    welcomeButton(localHumanTurnFirst);
  };
  welcome(humanTurnFirst);
});

EDIT:
The console output for console.logging corners in the filter callback is: Elements printed out in HTML
Whereas just document.getElementsByClassName('corner'): Node list of items

So, it must be something to do with the change in format that's causing the problem.

fifn2
  • 382
  • 1
  • 6
  • 15
  • Spread syntax is not a JavaScript operator https://stackoverflow.com/q/37151966/. Can you reproduce the issue at stacksnippets? See https://stackoverflow.com/help/mcve – guest271314 Feb 28 '19 at 02:16
  • @guest271314 What are you talking about? It's an EcmaScript 6 feature. They also said that they transpiled it with Babel. – Barmar Feb 28 '19 at 02:24
  • @Barmar Have you read the question and answers? `...` is not a JavaScript _operator_, it is a _punctuator_ http://www.ecma-international.org/ecma-262/6.0/#sec-punctuators – guest271314 Feb 28 '19 at 02:30
  • So you're just quibbling over terminology? – Barmar Feb 28 '19 at 02:31
  • @Barmar Always. – guest271314 Feb 28 '19 at 02:31
  • 1
    @guest271314 There, it's fixed. – Barmar Feb 28 '19 at 02:32
  • I can't see any way that an element of the array can be `undefined`. There must be more to your real code that's changing `$corners`. – Barmar Feb 28 '19 at 02:33
  • Try `$corners.filter((corner, i, corners) => console.log(corners, corner))` to debug. – yqlim Feb 28 '19 at 03:03
  • @YongQuan I have tried console.log(corner) inside the .filter callback. Interestingly, it is filtering correctly, but corner does not have an innerText property – fifn2 Feb 28 '19 at 03:06
  • In that weird case, maybe post your full code to give us a better context. It's most likely something else is wrong in your code. Not this filter function. – yqlim Feb 28 '19 at 03:07
  • @YongQuan Why didn't I think of that? It's here now. – fifn2 Feb 28 '19 at 03:18
  • Am I using the spread punctuator as an operator? Is that part of the problem? – fifn2 Feb 28 '19 at 15:24
  • @fifn2Confidential using spread syntax shouldn't be a problem from what you showed us. Looking at your code, it shouldn't have any problem. Can you update us on what the console return when you log `corner` inside the filter callback? – yqlim Mar 01 '19 at 09:17
  • @YongQuan Yeah, it's printing it out in this weird format. See my edit above. – fifn2 Mar 01 '19 at 15:10
  • Are you completely sure it's the mentioned line causing the error? You have also other `.innerText` accesses in the `.filter()` calls, e.x. `corner => !document.getElementsByClassName(\`${corner.className.split(/\s+/)[2].split('-')[1]}-edge\`)[0].innerText && !document.getElementsByClassName(\`${corner.className.split(/\s+/)[2].split('-')[2]}-edge\`)[0].innerText` which looks pretty bug prone – Sebastian Kaczmarek Mar 01 '19 at 15:15
  • Also, the *weird* format you see in the Chrome console is just the representation of the HTML Elements in the console. It just mean that those are HTML Elements and Chrome simply displays them like that – Sebastian Kaczmarek Mar 01 '19 at 15:17
  • 1
    Oh, and I think I found the bug. Shouldn't the `${corner.className.split(/\s+/)[2].split('-')[1]}-edge` be like this: `edge-${corner.className.split(/\s+/)[2].split('-')[1]}`? Notice that I moved the `-edge` to the beggining (`edge-`). – Sebastian Kaczmarek Mar 01 '19 at 15:24
  • 1
    Oh. My. God. @SebastianKaczmarek that's it! – fifn2 Mar 01 '19 at 15:31
  • Maybe I should've known. Those string methods looked a little fishy. – fifn2 Mar 01 '19 at 15:32

1 Answers1

1

So the error lies a few lines below, here:

const potentialCorners = adjacentCorners
        .filter(
          corner => !document.getElementsByClassName(`${corner.className.split(/\s+/)[2].split('-')[1]}-edge`)[0].innerText
            && !document.getElementsByClassName(`${corner.className.split(/\s+/)[2].split('-')[2]}-edge`)[0].innerText,
        );

You have classes like this: edge-* and not like this: *-edge so you have to change the selector here:

const potentialCorners = adjacentCorners
        .filter(
          corner => !document.getElementsByClassName(`edge-${corner.className.split(/\s+/)[2].split('-')[1]}`)[0].innerText
            && !document.getElementsByClassName(`edge-${corner.className.split(/\s+/)[2].split('-')[2]}`)[0].innerText,
        );

Also, the weird format you see in the Chrome console is just the representation of the HTML Elements in the console. It just mean that those are HTML Elements and Chrome simply displays them like that

Sebastian Kaczmarek
  • 8,120
  • 4
  • 20
  • 38