Home > database >  Cannot get function to acknowledge objects returned in array
Cannot get function to acknowledge objects returned in array

Time:09-09

I am trying to create a game like Text Twist (https://texttwist2.co/) and have gotten the initial layout of the game set up. I have 6 empty divs that are each populated with button on which one of the letters is displayed:

let startButton = document.getElementById("start");
let letterButtonArray = [];
startButton.addEventListener("click", function startRound() {

  let wordLettersShuffled = ['A', 'R', 'D', 'A', 'O', 'B']

  let letterBalls = document.getElementsByClassName("letter-ball");
  for (let i = 0; i < letterBalls.length; i  ) {
    let letterButton = document.createElement("button");
    letterButton.innerHTML = wordLettersShuffled[i];
    letterButton.setAttribute("class", "letter-button");
    letterButton.addEventListener("click", moveLetter);
    letterBalls[i].appendChild(letterButton);
    letterButtonArray.push(letterButton);
  }
  return letterButtonArray
}, {
  once: true
})

console.log(letterButtonArray) // 

function moveLetter() {
  alert(letterButton.innerHTML)
}
<div id="ball-container">
  <div ></div>
  <div ></div>
  <div ></div>
  <div ></div>
  <div ></div>
  <div ></div>
</div>

<button id="start">START GAME</button>

When I click one of the letterButtons I get "Uncaught ReferenceError: letterButton is not defined at HTMLButtonElement.moveLetter".

When I passed the letterButton as a parameter in the function, as:

function moveLetter(letterButton) {
    alert(letterButton.innerHTML)
}

an alert comes up but says "undefined".

I'm ultimately trying to move the individual letterButton to another div when clicked, but I can't get the moveLetter function to recognize the letterButtons that make up the letterButtonArray. Can anyone help? Thanks!

P.S. I console logged the letterButtonArray ahead of the moveLetter function to ensure that the array is returning the correct objects. It shows as below with the innerHTML noted:

0: button.letter-button  --> innerHTML: "A"
1: button.letter-button  --> innerHTML: "R"
2: button.letter-button  --> innerHTML: "D"
3: button.letter-button  --> innerHTML: "A"
4: button.letter-button  --> innerHTML: "O"
5: button.letter-button  --> innerHTML: "B"
length: 6
[[Prototype]]: Array(0)

CodePudding user response:

As Barmar commented earlier, "The argument passed to an event listener is an Event object, not the target of the event." When defining the event handler moveLetter, it is actually:

function moveLetter(event) {...` 

...by default. The argument event is an Event object that has built-in properties and methods that help us facilitate the control over the DOM when the registered event is triggered as well as some control over the registered event itself. The following table describes a few identifiers and properties that could be useful in the context of the event handler moveLetter(event):

JavaScript Description Context
.currentTarget References the element that the event handler/listener is attached to letterButton
.target References the actual element that the user is interacting with. This reference could be different than .currentTarget if the event handler is designed to employ event delegation in order to control multiple elements triggered by the registered event letterButton
this If within an event handler, it is .currentTarget letterButton

As you can see, there are a few ways to find the same object within an event handler but always be aware that .target is specific to whatever the user interacted with (ex. the element user clicked, entered text into, focused on/out, hovered over, etc.), which isn't always this or .currentTarget (but in this case it is because each individual button has it's own event handler).

In the example below, the event handler moveLetter(event) uses the event property .currentTarget. Commented source are alternatives. Also, the eventListener with the event handler moveLetter(event) has been commented out and in it's place is an eventListener with a new event handler called moveButton(event). moveButton will actually move a clicked button over to either div.word (which I added to the example) or div.ball. If you prefer the original event handler, uncomment:

// letterButton.addEventListener("click", moveLetter);

and comment or remove:

letterButton.addEventListener("click", moveButton);

I also removed the once option and added:

this.disabled = true`

...because it's not the best strategy if you intend to play the game on a single round without any pause between time allotted per word (messy and confusing UX). Just disable the start button when it's clicked then enable it at the end of each round.

One more change is that the array formally called wordLettersShuffled is now just letters and it is defined outside of the eventListener so it'll be easier to change to a new word every round. As a general rule, eventListeners, and event properties, etc are isolated and limited in certain ways so that specific elements involved with user interaction can be easily located and controlled. Try defining variables that change dynamically outside of event handlers/listeners or store values within elements instead of within an event handler/listener.

If you prefer not to define variables as I suggested, there are other ways far more complex but well worth using if under certain circumstances or if you intend to expand in the future (see Oskar Grosser's answer)

The function that is invoked when the registered event is triggered.

By convention the event object is commonly labeled as event, e, or evt. Since it's variable, it can be named just as any standard variable, but it should be clear and concise as possible.

const startButton = document.getElementById("start");
const ball = document.querySelector('.ball');
const word = document.querySelector('.word');

let letterButtonArray = [];
let letters = ['A', 'R', 'D', 'A', 'O', 'B'];

startButton.addEventListener("click", startRound);

function startRound(event) {
  for (let i = 0; i < letters.length; i  ) {
    let letterButton = document.createElement("button");
    letterButton.innerHTML = letters[i];
    letterButton.classList.add("letter-button");
    // letterButton.addEventListener("click", moveLetter);
    letterButton.addEventListener("click", moveButton);
    ball.append(letterButton);
    letterButtonArray.push(letterButton);
  }
  this.disabled = true;
}

function moveLetter(event) {
  console.log(event.currentTarget.innerHTML);
  /* Alternative 1 *///  console.log(event.target.innerHTML);
  /* Alternative 2 */// console.log(this.innerHTML);
}

function moveButton(event) {
  if (this.parentElement.matches('.ball')) {
    word.append(this);
  } else {
    ball.append(this);
  }
}
.box {
  min-height: 4ex
}
<div ></div>
<div ></div>

<button id="start">START GAME</button>

CodePudding user response:

As the error message suggests, no identifier of the name letterButton is found in the scope of moveLetter().

The scope (and the identifiers therein) that moveLetter is aware of is defined at function creation, which in this case means the global scope only. Therefore it is not aware of the for-loop's scope and its identifiers (letterButton).

Here are some nudges towards how you can solve your issue. If short on time, I recommend reading section "Using the event object".

Using closures

As mentioned before, a functions "knowledge" about identifiers is determined at function creation, even if the function is returned to a scope outside its initial scope. This is called a "closure".

Sidenote: Function statements are hoisted to the nearest context (function/global scope), but function expressions and arrow function expressions are scoped to the nearest scope (block/function/global scope).

We can make use of closures so that the moveLetter will know what identifier we mean with letterButton. Example:

for (const button of document.getElementsByTagName("button")) { // Block scope
  const clickHandler = function() {
    // Uses identifier `button` of the for-loop's block scope.
    // Therefore, it closes around that (the narrowest) scope.
    console.log(button.textContent);
  };
  
  button.addEventListener("click", clickHandler);
}
<button>Click me</button>
<button>Click me too!</button>

Sidenote: For-loops create a new scope for each iteration (when using let/const). That is why the similarly named identifier button references different objects for each iteration.

As you can see, the function is aware of the identifiers if it is in the same scope as them.

Creating the function anywhere inside the scope works, even inline in the addEventListener() call. Therefore, you may often see something similar to this:

const button = document.querySelector("button");
button.addEventListener("click", function() {
  // This function is created inline (anonymously).
  // It shares the same scope with identifier `button`, and is therefore aware of it.
  console.log(button.textContent);
});
<button>Click me</button>

Binding the arguments

We can bind certain arguments to a function (e.g. with Function.bind()). That way, no closure is created, and we can use the parameters of our function.

for (const button of document.getElementsByTagName("button")) {
  button.addEventListener("click", clickHandler.bind(null, button));
}

function clickHandler(button) {
  console.log(button.textContent);
}
<button>Click me</button>
<button>Click me too!</button>

Similarly, you may see something like this:

for (const button of document.getElementsByTagName("button")) {
  button.addEventListener("click", () => clickHandler(button));
}

function clickHandler(button) {
  console.log(button.textContent);
}
<button>Click me</button>
<button>Click me too!</button>

Here, the arrow function creates a closure around button, but passes it as an argument to clickHandler(). Therefore, we effectively "bind" the argument to clickHandler(). Personally I find that this is just an unclean way of binding arguments.

Using the event object

Since moveLetter is added via EventTarget.addEventListener(), it is passed the current event object as its first argument.

Using event.target, we can find the clicked button.

Sidenote: If a descendant of the listening element is clicked, event.target will reference the clicked descendant. We can use Element.closest() to find the relevant (ancestral?) element.

for (const button of document.getElementsByTagName("button")) {
  button.addEventListener("click", clickHandler);
}

function clickHandler(evt) {
  // Using `closest()` is unnecessary here, because our buttons don't have any children.
  // We can still use it though.
  const button = evt.target.closest("button");
  console.log(button.textContent);
}
<button>Click me</button>
<button>Click me too!</button>

Using the event object allows us to use even another method, which I'd prefer:

Using event delegation

Event delegation describes the use of one event listener of a common ancestor to provide functionality to multiple elements. As we learned, we can find the relevant element using event.target and optionally with Element.closest().

The above example may therefore look like this:

const commonAncestor = document.body;
commonAncestor.addEventListener("click", clickHandler);

function clickHandler(evt) {
  const button = evt.target.closest("button");
  if (button === null) {
    // No button was clicked
    return;
  }
  
  console.log(button.textContent);
}
<button>Click me</button>
<button>Click me too!</button>

The advantage of this is that it uses less listeners and therefore less memory, scales easier (new elements will come with "attached" listeners), and you don't have to worry about potentially destroying your listeners (e.g. by assigning to innerHTML of your element).

The disadvantage of this is that it may be complicated to understand, and that the listener is declared elsewhere than where the elements are created.

Generally, I suggest to use event delegation to reduce duplicate code and have the browser perform optimizations as to which elements should react to given events.

  • Related