-1

var selectedLetter = "global right now";

function GetLink() {
  fetch("https://random-word-api.herokuapp.com/word")
    .then(
      r => {
        if (r.status != 200)
          return;
        r.json().then(
          c => {
            for (let i = 0; i < c[0].length; i++) {
              p = document.createElement("div");
              p.innerHTML = "_";
              if (i == 0) {
                p.setAttribute("id", "first");
              }
              p.classList.add("word");
              document.querySelector("#word").appendChild(p)
            }

          });
      }
    )
}

function SetupKeyboard() {
  letters = document.getElementsByClassName("word");
  selectedLetter = document.getElementById("first");
}

window.addEventListener('load', () => {
  GetLink();
  SetupKeyboard();
});
html,
body,
#wrapper {
  height: 100%;
  width: 100%;
}

#title {
  font-size: xx-large;
  text-align: center;
}

#word {
  font-size: xx-large;
  text-align: center;
}

.word {
  display: inline;
  margin: 2px;
  background-color: beige;
}
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Hangman</title>
  <link rel="stylesheet" href="style.css">
  <script src="index.js"></script>
</head>

<body>
  <div id="title">
    Hangman
  </div>
  <div id="word">

  </div>
</body>

</html>

It seems to be a simple problem but it's driving me insane.

function SetupKeyboard()
{
  letters=document.getElementsByClassName("word");    
  selectedLetter=document.getElementById("first");
}

I need the selected letter to be the first element of the "letters" array but for some reason selectedLetter=letters[0] gave back "undefined". I tried this approach too and it's the same problem. I am obviously missing something.

When I debug the code it works properly and the console logs everything as expected. When NOT in debugger the value of selectedLetter is "undefined". Also, the problem is gone when I call the function manually through browser console as well.

I know the problem is not in the DOM not existing because I run it all like this:

window.addEventListener('load', ()=>{
    GetLink();
    SetupKeyboard();
});
Daniel Beck
  • 20,653
  • 5
  • 38
  • 53
  • 1
    There's not enough information here to go on; the problem is somewhere in code you're not showing us. But if element 0 of an array (well, arraylike collection object in this case) is undefined, that rather strongly suggests that the whole thing is empty. – Mark Reed Jul 20 '22 at 12:59
  • The array itself is functional and it logs out its contents. The GetLink() function just grabs a random word from an API and makes the div elements with the class name "word". – Alagić Senad Jul 20 '22 at 13:01
  • 1
    It would be really helpful if you set up a [mcve] (use the "snippet" tool, under the icon with the `<>`) so we can see what's actually going on -- from your description it's not clear what you specifically mean by "when I debug the code" and "when NOT in debugger". (Assuming you mean the in-browser debugger, there's no difference: you're either looking at the debugger output or you aren't, it doesn't change what your code is doing either way.) – Daniel Beck Jul 20 '22 at 13:02
  • Does this answer your question? [Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference](https://stackoverflow.com/q/23667086/328193) – David Jul 20 '22 at 13:10
  • I hope this is what you meant by minimal reproducible example. Also, to clarify: "when I debug the code" means "when I go step by step", and "when NOT in debugger" means "when I run the code normally". Hope that helps :) – Alagić Senad Jul 20 '22 at 13:11
  • I have a general grasp of async functions but I wasn't really sure this was the case. My understanding is that even though it's an API call that takes some time, it's wrapped in a sync function ("normal") therefore everything does wait for it's execution? – Alagić Senad Jul 20 '22 at 13:17
  • @AlagićSenad: That assumption is mistaken. Asynchronous operations are asynchronous. Wrapping them in synchronous functions doesn't change that, it just hides from consuming code the asynchronous operations being performed so consuming code *can't* await them. Which is generally a bad thing. – David Jul 20 '22 at 13:22
  • 1
    PLEASE do not edit the question to incorporate the suggestions made in the answers! That will make this question useless to future users, since the answers will refer to code that is no longer visible – Daniel Beck Jul 20 '22 at 13:25

2 Answers2

1

GetLink() is performing an asynchronous operation. Which means when SetupKeyboard() executes, that asynchronous operation has not yet completed and the target elements you're looking for don't exist.

Since fetch returns a Promise, you can return that from your function:

function GetLink() {
  return fetch("https://random-word-api.herokuapp.com/word")
  // the rest of the function...
}

Then, exactly as you do in your GetLink function, you can append .then() to that Promise to follow it up:

window.addEventListener('load', ()=>{
  GetLink().then(SetupKeyboard);
});

Note the lack of parentheses on SetupKeyboard. We're passing the function itself to .then(). This is structurally similar to using an explicit anonymous function to wrap the SetupKeyboard() call, like you do in your .then() callbacks already:

window.addEventListener('load', ()=>{
  GetLink().then(() => SetupKeyboard());
});

Edit: You appear to also be stacking/chaining your asynchronous operations incorrectly within GetLink. Which means that my suggestion above is awaiting the first asynchronous operation, but not the second. Make GetLink an async function to make the syntax cleaner, and await the asynchronous operations therein:

async function GetLink()
{
  let r = await fetch("https://random-word-api.herokuapp.com/word");
  if (r.status != 200)
    return;
  let c = await r.json();
  for (let i = 0; i < c[0].length; i++) {
    p = document.createElement("div");
    p.innerHTML = "_";
    if (i == 0) {
      p.setAttribute("id", "first");
    }
    p.classList.add("word");
    document.querySelector("#word").appendChild(p)
  }
}

This not only makes the syntax much cleaner and easier to follow, but effectively it chains all of the await operations into one resulting Promise chain, which you can then follow up:

window.addEventListener('load', ()=>{
  GetLink().then(SetupKeyboard);
});
David
  • 208,112
  • 36
  • 198
  • 279
  • I did all of that but the SetupKeyboard function doesn't run at all now. Check the reprex – Alagić Senad Jul 20 '22 at 13:22
  • @AlagićSenad: *"the SetupKeyboard function doesn't run at all now"* - That is demonstrably false with some basic debugging, even simply placing a `console.log` statement within that function. **However**, there is a remaining issue in the code. I've updated my answer to account for it. Basically the `GetLink` function has *multiple* asynchronous operations, and they weren't being chained properly. – David Jul 20 '22 at 13:29
  • @David, can you mix async and Promises syntax like that? (using `async` on the function but then waiting for it using `then()`?) Sincere question, I've never tried that – Daniel Beck Jul 20 '22 at 13:30
  • @DanielBeck: Of course. A [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) explicitly provides the now-standard/common structure of `.then()` and `.catch()` callbacks. And its structure also matches what the language considers to be "awaitable" and so can use the `await` keyword instead where applicable. (I'd have to look it up, but I think that structure is what *makes* it "awaitable".) Things get confusing when combining both *in the same statement*, such as: `await someAsyncFunc().then(/*...*/);`, so *that* should generally be avoided. – David Jul 20 '22 at 13:32
  • Cool, I wasn't aware the syntaxes were interchangeable. Good to know, thanks! – Daniel Beck Jul 20 '22 at 13:33
  • Thank you so much. This helped me. I'm not *quite* 100% sure what was the cause, chaining multiple async functions seems to be it. I will do it like this in the future. Again, many thanks and sorry for your inconvenience! – Alagić Senad Jul 20 '22 at 13:33
0

The answer was hidden in your comments:

The GetLink() function just grabs a random word from an API and makes the div elements with the class name "word".

GetLink() makes an API call, so is therefore asynchronous; so you're calling SetupKeyboard before GetLink has inserted the DOM elements SetupKeyboard is looking for.

You should instead ensure GetLink returns a promise, and do something like this in your window.load handler:

GetLink().then(SetupKeyboard)

...or else use async / await -- same thing, different syntax.

My understanding is that even though it's an API call that takes some time, it's wrapped in a sync function ("normal") therefore everything does wait for it's execution?

You've got it backwards; code will wait for async functions, if you explicitly tell it to do so using await or wrapping it in a Promise's then().

(Personally I think the Promises syntax is less confusing, especially for people new to the concept; I would suggest sticking with that until you're more comfortable working with asynchronous code.)

Daniel Beck
  • 20,653
  • 5
  • 38
  • 53