2

I am trying to create a typing effect using Vanilla JS, but for some reason the charAt function isn't working, and when I replace i with something like 0, it works, but it spits it all out at once even though it's wrapped in a setTimeout() function

var sentence = document.getElementsByClassName('sentence')[0];
var words = ['websites', 'apps', 'games'];
var speed = 100;

function type(word) {
  for(var i = 0; i < word.length; i++) {
    setTimeout(function() {
      sentence.innerHTML += word.charAt(i);
    }, speed);
  }
}

type(words[0]);
* {
  font-family: Arial;
}

.container {
  display: flex;
  align-items: center;
  justify-content: center;
}

.cursor {
  background: #000;
  width: 2px;
  height: 15px;
  animation: blink 1s steps(5, start) infinite;
}

@keyframes blink {
  to { visibility: hidden; }
}
<div class="container">
  <div class="sentence">We make </div>
  <div class="cursor"></div>
</div>
le_m
  • 19,302
  • 9
  • 64
  • 74
Jordan Baron
  • 3,752
  • 4
  • 15
  • 26
  • 2
    Related: https://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example - possibly even dupe – Caramiriel Jan 19 '18 at 20:11
  • Use `let i` instead of `var i` in the for loop and write `speed * i` instead of just `speed` as the second argument for `setTimeout()`. – le_m Jan 19 '18 at 20:12
  • See also: https://stackoverflow.com/questions/46992706/settimout-not-working-inside-for-loop-acting-weird – le_m Jan 19 '18 at 20:18
  • https://stackoverflow.com/questions/5226285/settimeout-in-for-loop-does-not-print-consecutive-values – Salman A Jan 19 '18 at 20:48

4 Answers4

2

I'm pretty sure switching in this code block for your timeout will solve your issue. I haven't had the ability to test it myself though.

setTimeout(function(i) {
    sentence.innerHTML += word.charAt(i);
}.bind(this,i), speed * i);
Poe
  • 126
  • 6
2

Use an "asynchronous" loop using recursion, because now you start all your timers at once:

var sentence = document.getElementsByClassName('sentence')[0];
var words = ['websites', 'apps', 'games'];
var speed = 100;

function type(word) {
   if (!word.length) return; // Nothing to do
   setTimeout(function() {
      sentence.textContent += word.charAt(0);
      type(word.substr(1)); // call recursively only now
   }, speed);
}

type(words[0]);
* {
  font-family: Arial;
}

.container {
  display: flex;
  align-items: center;
  justify-content: center;
}

.cursor {
  background: #000;
  width: 2px;
  height: 15px;
  animation: blink 1s steps(5, start) infinite;
}

@keyframes blink {
  to { visibility: hidden; }
}
<div class="container">
  <div class="sentence">We make </div>
  <div class="cursor"></div>
</div>
trincot
  • 317,000
  • 35
  • 244
  • 286
  • Accepted because of the use of recursion and because it's the shortest amount of code. – Jordan Baron Jan 20 '18 at 10:14
  • One question though. Why aren't all the timeouts created at once? Going by what I know from the other answers the timeouts should be created at once. – Jordan Baron Jan 20 '18 at 11:42
  • In my answer a timeout is only created when the previous one has expired. The difference is that then the timeout argument is always `speed`, while if you create them at once, you need to supply a different value to each of them (through multiplication). – trincot Jan 20 '18 at 12:08
1

You probably want to use recursion so that each time a new letter is added to the innerHtml, it starts a new timeout. Right now it's creating all your timeouts at the same time so they all fire basically at the same time.

MaddawgX9
  • 131
  • 8
  • but why are the timeouts being created at once? I thought the timeouts ran, waited for the specified time, then continued with the following code? – Jordan Baron Jan 19 '18 at 20:25
  • @JordanBaron `setTimeout()` starts a timer that will run the specified function once the time is up, but the rest of your code keeps running and doesn't wait for the timer to complete. – Herohtar Jan 19 '18 at 20:32
  • @JordanBaron the code first loops and creates word.length number of timers. All of these timers are set to go off in 100ms. So 100ms later, all of them fire almost at once (they are all slightly offset because it takes a very small amount of time to go through each iteration of your loop) and add their letters in. – MaddawgX9 Jan 19 '18 at 20:42
1

You should increase the speed as 100 is very low and you can't see it. "websites" is still loading in 800ms so it is hard to see anything.

Don't use "type" as your function name as this is a reserved jquery function.

This is how it works:

var sentence = document.getElementsByClassName('sentence')[0];
var words = ['websites', 'apps', 'games'];
var speed = 100;

function typewriter_string(word) {
  sentence.textContent='';
  for(var i = 0; i < word.length; i++) {
      doSetTimeout(i, word);
  }
}

function doSetTimeout(i, word){
    setTimeout(function() {
      sentence.textContent += word.charAt(i);
    }, speed*i);
}

typewriter_string('websites');

In your code the "i" is always 8 because of the setTimeout in the for-loop.

Janine Kroser
  • 444
  • 2
  • 6
  • 23