0

I have a HTML file with some JS logic where I want to create buttons based on an array.

for (var i = 0; i < arrayButtons.length; i++) {
            console.log("console! ", arrayButtons[i].title); // works!
            let buttonTitle = arrayButtons[i].title;

            document.getElementById("buttons").innerHTML +=
                "<div><button class='changeButton' " +
                       "onclick= document.getElementById(\"changeByButtonClick\").innerHTML = buttonTitle'>" 
                      + "<i class=\"icon\"></i>"+ buttonTitle + "</button></div>";}

In this line: "onclick= document.getElementById(\"changeByButtonClick\").innerHTML = buttonTitle'>", it throws an error that buttonTitle is not defined:

Uncaught ReferenceError: buttonTitle is not defined at HTMLButtonElement.onclick

I absolutely don't understand why. As you can see, I defined buttonTitle inside the method and just want to access it. And on console.log(), it works. Why is it undefined in onClick()?

Unfortunately, this and this solutions don't work for me.

Help will be much appreciated!

RuntimeError
  • 1,332
  • 4
  • 23
  • 41
  • `buttonTitle` is defined in a loop, presumably in a function. The reference to that variable is in HTML in the global scope. – Heretic Monkey Apr 27 '20 at 12:22
  • Does this answer your question? [What is the scope of variables in JavaScript?](https://stackoverflow.com/questions/500431/what-is-the-scope-of-variables-in-javascript) – Heretic Monkey Apr 27 '20 at 12:22
  • 1
    @HereticMonkey - Probably not, the OP seems to think the code in the string closes over the `buttonTitle` in the function. The key bit here is that code in `onclick` attribute strings is evaluated outside that function. – T.J. Crowder Apr 27 '20 at 12:23
  • 1
    **Please** don't hide closing `}` like that. It's nearly impossible to read and maintain. :-) Whoever it is out there promoting that style is doing a lot of (minor) harm to up-and-coming programmers. – T.J. Crowder Apr 27 '20 at 12:25
  • 1
    @T.J.Crowder The fact that `buttonTitle` is declared using `let` means its scope is limited to the `for` it is declared within, something covered by that duplicate. Where the code in the `onclick` attribute is evaluated is irrelevant inasmuch as it isn't evaluated within the `for` loop. – Heretic Monkey Apr 27 '20 at 12:28
  • @HereticMonkey - That's my point, I think the OP thought it was evaluated within the loop. – T.J. Crowder Apr 27 '20 at 12:29

1 Answers1

2

Your first use of buttonTitle is inside the string that you're assigning to the innerHTML of the id="buttons" element, inside the onclick attribute you're using there. The code in the onclick string is evaluated at global scope,¹ where you don't have a buttonTitle variable.

I strongly recommend not building code strings like that, not least for this reason. Instead, use a function:

for (var i = 0; i < arrayButtons.length; i++) {
    console.log("console! ", arrayButtons[i].title); // works!
    let buttonTitle = arrayButtons[i].title;

    const buttons = document.getElementById("buttons")
    buttons.insertAdjacentHTML(
        "beforeend",
        "<div><button class='changeButton'>" +
        "<i class=\"icon\"></i>"+ buttonTitle + "</button></div>"
    );
    // Get the button we just added (the last one)
    const buttonList = buttons.querySelectorAll("button");
    const newButton = buttonList[buttonList.length - 1];
    // Add the handler to it
    newButton.addEventListener("click", function() {
        document.getElementById("changeByButtonClick").innerHTML = buttonTitle;
    });
}

Live Example:

const arrayButtons = [
    {title: "first"},
    {title: "second"},
    {title: "third"},
    {title: "fourth"},
];

for (var i = 0; i < arrayButtons.length; i++) {
    console.log("console! ", arrayButtons[i].title); // works!
    let buttonTitle = arrayButtons[i].title;

    const buttons = document.getElementById("buttons")
    buttons.insertAdjacentHTML(
        "beforeend",
        "<div><button class='changeButton'>" +
        "<i class=\"icon\"></i>"+ buttonTitle + "</button></div>"
    );
    // Get the button we just added (the last one)
    const buttonList = buttons.querySelectorAll("button");
    const newButton = buttonList[buttonList.length - 1];
    // Add the handler to it
    newButton.addEventListener("click", function() {
        document.getElementById("changeByButtonClick").innerHTML = buttonTitle;
    });
}
#changeByButtonClick {
    min-height: 1em;
}
<div id="changeByButtonClick"></div>
<div id="buttons">
    This is the initial content that we don't remove.
</div>

Also note that I used insertAdjacentHTML instead of using += on innerHTML. (Thanks to Niet the Dark Absol for pointing it out, I missed it was a +=.) Never use += on innerHTML, when you do that, the browser has to do this:

  • Recurse through the nodes in the target element building an HTML string.
  • Pass that string to the JavaScript layer.
  • Receive the new string from the JavaScript layer.
  • Tear down the entire contents of the target element.
  • Parse the string.
  • Build new replacement nodes for the previous contents plus the new nodes for the new content.

In the process it loses event handlers and other non-HTML information. In contrast, with insertAdjacentHTML, all it has to do is parse the string and insert the new nodes.


¹ "...at global scope..." Actually, it's a bit more complicated than that, it's a nested scope using (effectively) with blocks. But for the purposes of the question, the details there aren't important; it's nearly at global scope.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • 2
    I would suggest using `insertAdjacentHTML`, rather than clobbering with `innerHTML +=` - especially as this clobber would remove the event handlers that you're adding. – Niet the Dark Absol Apr 27 '20 at 12:25
  • @NiettheDarkAbsol - Yeah, I missed that it was a `+=`! Fixed and noted. – T.J. Crowder Apr 27 '20 at 12:26
  • Wow, thanks a lot! I learned a lot here :) However the line with adding the eventListener still seems not to work, because the element it's supposed to change still remains the same. And there is no error message after all >.< But, scope of the question perfectly answered. Thanks and +1 – RuntimeError Apr 27 '20 at 12:34
  • @RuntimeError - Sorry, I needed to update the code hooking the button up to account for the fact that the original code was *adding to* the element's contents (`+=`), not replacing it (`=`). So my code was only adding handlers to the first button, not the one we just added. Fixed now. – T.J. Crowder Apr 27 '20 at 12:38
  • @T.J.Crowder Oh, now it only creates one single button ^^ Problem seems to be this: newButton.addEventListener. There it says, newButton is undefined after creating the first button. – RuntimeError Apr 27 '20 at 12:47
  • @RuntimeError - The code above creates multiple buttons if there are multiple entries in the array. I've added a runnable example demonstrating it. It doesn't have that error. – T.J. Crowder Apr 27 '20 at 12:51
  • 1
    @T.J.Crowder Sorry, I accidentally deleted a char xD Again, thanks a lot. You saved my day :) – RuntimeError Apr 27 '20 at 13:07