Reputation:
In the following code, I expect that it gets the words in all of the .auto-type
elements as input, then starts typing them out character-by-character automatically. However, the code I have doesn't work, but it also doesn't produce any errors in the console. What have I missed?
window.onload = () => {
let elements = document.getElementsByClassName("auto-type");
for (element of elements) {
const text = element.innerHTML.toString();
element.innerHTML = "";
let charIndex = 0;
const typing = () => {
element.innerHTML += text.charAt(charIndex);
if (charIndex < text.length) {
setTimeout(typing, 100);
charIndex++;
}
};
typing();
}
};
body {
background-color: black;
color: lime;
font-family: consolas, monospace;
}
<span class="auto-type code">hello!</span>
<br />
<span class="auto-type code">some text</span>
<br />
<div class="auto-type">some other text</div>
<span class="auto-type code">here is a better text</span>
Upvotes: 0
Views: 58
Reputation: 26196
The code works but because it's asynchronous, it just happens to be trying to type it all out at the same time. The other reason that it isn't making sense is because it is also trying to type it into the same element.
Once rendered, you see this in the HTML of the page:
<span class="auto-type code">h</span>
<br>
<span class="auto-type code">s</span>
<br>
<div class="auto-type">s</div>
<span class="auto-type code">heooelmmrleeeo !toietsxh tear bteetxtter text</span>
Focussing on that last line, you can see that each message has been typed into the last element. This can be made more obvious by replacing each character in your original messages with a number:
<span class="auto-type code">111111</span>
<br />
<span class="auto-type code">222222222</span>
<br />
<div class="auto-type">333333333333333</div>
<span class="auto-type code">444444444444444444444</span>
Which renders:
<span class="auto-type code">1</span>
<br>
<span class="auto-type code">2</span>
<br>
<div class="auto-type">3</div>
<span class="auto-type code">412341234123412341234234234234343434343434444444</span>
So that last line is built up like this:
heooelmmrleeeo !toietsxh tear bteetxtter text
│││└ the 'e' from "here" in the fourth element
││└ the 'o' from "some" in the third element
│└ the 'o' from "some" in the second element
└ the 'e' from "hello" in the first element
The reason this happens is because of this for-loop, where the variable element
is not scoped to the for-loop, meaning that it gets shared by each typing
function, and will refer to the last element in elements
:
for (element of elements) { // <- this element variable is a global!
/* ... */
}
You can solve your primary issue by simply redefining that element
variable with let
or const
instead:
for (const element of elements) { // <- this element variable is now scoped to this for-loop
/* ... */
}
The effect of this change can be seen in the below StackSnippet:
window.onload = () => {
let elements = document.getElementsByClassName("auto-type");
for (const element of elements) {
const text = element.innerHTML.toString();
element.innerHTML = "";
let charIndex = 0;
const typing = () => {
element.innerHTML += text.charAt(charIndex);
if (charIndex < text.length) {
setTimeout(typing, 100);
charIndex++;
}
};
typing();
}
};
body {
background-color: black;
color: lime;
font-family: consolas, monospace;
}
<span class="auto-type code">hello!</span>
<br />
<span class="auto-type code">some text</span>
<br />
<div class="auto-type">some other text</div>
<span class="auto-type code">here is a better text</span>
Because of the nature of JavaScript, it can take a while to load your page and all your dependencies. This means your messages would be visible to the user while the page is loading which probably isn't what you are looking for. So you should hide the messages with CSS and then reveal them when you are ready.
.auto-type {
visibility: hidden; /* like display:none; but still consumes space on the screen */
}
Then when you empty the text out, make the element visible again:
const text = element.innerHTML.toString();
element.innerHTML = "";
element.style.visibility = "visible";
You need to rework your "type into" function to not fire off until the previous message has been typed out. We can do this by updating each "type into" function to return a Promise and then by chaining those Promises together.
/**
* Types text into the given element, returning a Promise
* that resolves when all the text has been typed out.
*/
function typeTextInto(element, text, typeDelayMs = 100) {
return new Promise(resolve => {
let charIndex = 0;
const typeNextChar = () => {
element.innerHTML += text.charAt(charIndex);
if (charIndex < text.length) {
setTimeout(typeNextChar, typeDelayMs);
charIndex++;
} else {
resolve(); // finished typing
}
};
typeNextChar(); // start typing
});
}
window.onload = () => {
const elements = document.getElementsByClassName("auto-type");
const elementTextPairs = [];
// pass 1: empty out the contents
for (element of elements) {
const text = element.innerHTML.toString();
element.innerHTML = "";
element.style.visibility = "visible";
elementTextPairs.push({ element, text });
}
// pass 2: type text into each element
let chain = Promise.resolve(); // <- empty Promise to start the chain
for (elementTextPair of elementTextPairs) {
const { element, text } = elementTextPair; // unwrap pair
chain = chain // <- add onto the chain
.then(() => typeTextInto(element, text));
}
// basic error handling
chain.catch(err => console.error("failed to type all messages:", err));
};
This can be futher cleaned up using async
/await
and splitting the typing logic out into its own function. This is shown in the working StackSnippet below:
/**
* Types text into the given element, returning a Promise
* that resolves when all the text has been typed out.
*/
function typeTextInto(element, text, typeDelayMs = 100) {
return new Promise(resolve => {
let charIndex = 0;
const typeNextChar = () => {
element.innerHTML += text.charAt(charIndex);
if (charIndex < text.length) {
setTimeout(typeNextChar, typeDelayMs);
charIndex++;
} else {
resolve(); // finished typing
}
};
typeNextChar(); // start typing
});
}
window.onload = async () => {
try {
const elements = document.getElementsByClassName("auto-type");
const elementTextPairs = [];
// pass 1: empty out the contents
for (element of elements) {
const text = element.innerHTML.toString();
element.innerHTML = "";
element.style.visibility = "visible";
elementTextPairs.push({ element, text });
}
// pass 2: type text into each element
for (elementTextPair of elementTextPairs) {
const { element, text } = elementTextPair; // unwrap pair
await typeTextInto(element, text); // await in a for-loop makes the asynchronous work run one-after-another
}
} catch (err) {
// basic error handling
console.error("failed to type all messages:", err);
}
};
body {
background-color: black;
color: lime;
font-family: consolas, monospace;
}
.auto-type {
visibility: hidden;
}
<span class="auto-type code">hello!</span>
<br />
<span class="auto-type code">some text</span>
<br />
<div class="auto-type">some other text</div>
<span class="auto-type code">here is a better text</span>
Upvotes: 1