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.
Eventobject, not the target of the event. You needletterButton.target.innerHTML