29

I'm trying to write a function (in JavaScript) that would write a sentence in a <p> tag by writing its letters one by one with a 300ms pause between each letter, for exmaple. I've written the following:

        var text = ["H", "e", "l", "l", "o", " ", "h", "o", "w", " ", "a", "r", "e", "y", "o", "u", "?"]
        function typeText() {
            var i = 0;
            var interval = setInterval(function () {
                var parag = document.getElementById("theParagraph");
                var paragOldText = parag.innerText;
                parag.innerText = paragOldText + text[i];
                i++;
                if (text.length == i)
                    clearInterval(interval);
            }, 200)
        }
<body>
    <p id="theParagraph"></p>
    <button id="typeButton" onclick="typeText()" style="padding:15px">Start typing the sentence</button>
</body>

As you can see, there are some " " (empty space) characters in the array; the problem is that it doesn't write those empty spaces, so the sentence would be like this: "Hellohowareyou". How do I solve this?

Arad Alvand
  • 8,607
  • 10
  • 51
  • 71
  • 18
    Since none of the other answers explain *why* your code doesn’t work: in a nutshell, it’s because [`innerText` represents the rendered text on a page](https://stackoverflow.com/a/19032002/1968), which means applying the element’s [`white-space` rules](https://developer.mozilla.org/en-US/docs/Web/CSS/white-space). The effect in your case is that surrounding whitespace is stripped off. – Konrad Rudolph Dec 12 '17 at 10:40
  • 3
    Have a look at the [Difference between text content vs inner text](https://stackoverflow.com/q/35213147/1048572) and possibly also [this](https://stackoverflow.com/a/17203046/1048572) – Bergi Dec 12 '17 at 14:36
  • Read Bergi's comment^ – Zack Plauché Jun 24 '20 at 06:05

7 Answers7

35

Don't use presentation as data. Store the current content as a separate string, don't pull it from the DOM. This way you're not dependent on how the browser stores the element's text content.

var text = ["H", "e", "l", "l", "o", " ", "h", "o", "w", " ", "a", "r", "e", "y", "o", "u", "?"]
 
function typeText() {
    var i = 0;
    var paragText = "";
    var interval = setInterval(function () {
        var parag = document.getElementById("theParagraph");
        paragText += text[i];
        parag.innerText = paragText;
        i++;
        if (text.length == i)
            clearInterval(interval);
    }, 200)
}
<body>
    <p id="theParagraph"></p>
    <button id="typeButton" onclick="typeText()" style="padding:15px">Start typing the sentence</button>
</body>

As a side note, the same thing could be made a lot simpler:

var text = "Hello how are you?";

function typeText() {
    var i = 0;
    var interval = setInterval(function () {
        var parag = document.getElementById("theParagraph");
        parag.innerText = text.substr(0, i);
        if (text.length == i)
            clearInterval(interval);
        i++;
    }, 200)
}
<body>
    <p id="theParagraph"></p>
    <button id="typeButton" onclick="typeText()" style="padding:15px">Start typing the sentence</button>
</body>
JJJ
  • 32,902
  • 20
  • 89
  • 102
  • 2
    While the other answers are certainly correct, and contain helpful information, this answer brought up something important that didn't even occur to me. gj – John Wu Dec 12 '17 at 10:31
19

What about using textContent?

var text = ["H", "e", "l", "l", "o", " ", "h", "o", "w", " ", "a", "r", "e", " ","y", "o", "u", "?"]

function typeText() {
  var i = 0;
  var interval = setInterval(function() {
    var parag = document.getElementById("theParagraph");
    var paragOldText = parag.textContent;
    parag.textContent = paragOldText + text[i];
    i++;
    if (text.length == i)
      clearInterval(interval);
  }, 200)
}
<body>
  <p id="theParagraph"></p>
  <button id="typeButton" onclick="typeText()" style="padding:15px">Start typing the sentence</button>
</body>

You can also use innerHTML:

var text = ["H", "e", "l", "l", "o", " ", "h", "o", "w", " ", "a", "r", "e", " ", "y", "o", "u", "?"]

function typeText() {
  var i = 0;
  var interval = setInterval(function() {
    var parag = document.getElementById("theParagraph");
    var paragOldText = parag.innerHTML;
    parag.innerHTML = paragOldText + text[i];
    i++;
    if (text.length == i)
      clearInterval(interval);
  }, 200)
}
<body>
  <p id="theParagraph"></p>
  <button id="typeButton" onclick="typeText()" style="padding:15px">Start typing the sentence</button>
</body>

innerText was introduced by IE and, as we all know, nothing good comes from IE. Joking apart, this is a good explanation about it: "The poor, misunderstood innerText".

Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171
  • Everything written about browser compatibility gets old pretty quicky nowadays, also FF has adapted `innerText` lately. – Teemu Dec 12 '17 at 10:51
  • Thank you! this has helped me. but it would be better if you tell the cause – Arad Alvand Dec 12 '17 at 12:14
  • 2
    @Teemu More accurately, [it's been adopted by WHATWG lately](https://html.spec.whatwg.org/multipage/dom.html#the-innertext-idl-attribute). – Bob Dec 13 '17 at 02:16
4

The other answers address the issues with your code, but I'd like to address issues with your whole plan.

  • Do you really want to be defining an array of characters? Long sentences are going to be hell. And what if you want variable text? Use this instead:

    var input = "Hello how are you?";
    var text = input.split(""); // split into array of characters
    
  • Speaking of longer sentences, your "typewriter" will fill out the current line, realise it doesn't have room, and then bump the last word down to the next line to finish it. This is not a good look! You can get around this with a clever trick:

    <p><span id="visible_text">Hello how a</span><span id="remaining_text">re you?</span></p>
    <style>#remaining_text {visibility:hidden}</style>
    

    Not only will this handle word wrapping very nicely, it will also "reserve" the necessary space ahead of time so that you don't end up with it pushing the content below the typewriter further down the page as new lines arise.

    You can easily achieve this effect by counting which character position you are at, then splitting the input string into two pieces at that offset. Put the first piece in the first <span>, the rest in the second, and you're golden.

Source: I use this technique in my "RPG cutscene"-style code. Actually a more advanced version, as mine also supports HTML rather than just plain text!

Niet the Dark Absol
  • 320,036
  • 81
  • 464
  • 592
1

You need to introduce the space using &nbsp; and use innerHTML instead of innerText

var paragOldText = parag.innerHTML;
parag.innerHTML = paragOldText + ( text[i].trim().length ? text[i] : "&nbsp;" ) ;

Edit

&nbsp; isn't required with innerHTML

var paragOldText = parag.innerHTML;
parag.innerHTML = paragOldText + text[i] ;

Demo

var text = ["H", "e", "l", "l", "o", " ", "h", "o", "w", " ", "a", "r", "e", "y", "o", "u", "?"]

function typeText() {
  var i = 0;
  var interval = setInterval(function() {
    var parag = document.getElementById("theParagraph");
    var paragOldText = parag.innerHTML;
    parag.innerHTML = paragOldText + text[i];
    i++;
    if (text.length == i)
      clearInterval(interval);
  }, 200)
}
<body>
  <p id="theParagraph"></p>
  <button id="typeButton" onclick="typeText()" style="padding:15px">Start typing the sentence</button>
</body>
gurvinder372
  • 66,980
  • 10
  • 72
  • 94
1

I've modified your code to show how you can use the slice method for shorter, more elegant code.

var text = "Hello how are you?"
function typeText() {
var i = 0;
var parag = document.getElementById("theParagraph");
var interval = setInterval(function () {
    i++;
    parag.innerText = text.slice(0, i);
    if (i == text.length)
        clearInterval(interval);
    }, 200)
}
<body>
    <p id="theParagraph"></p>
    <button id="typeButton" onclick="typeText()" style="padding:15px">Start typing the sentence</button>
</body>
Chris Rollins
  • 550
  • 3
  • 9
0

Short answer: Use textContent attribute instead of innerText attribute and you'll be able to add spaces.

e.g.

var text = ["H", "e", "l", "l", "o", " ", "h", "o", "w", " ", "a", "r", "e", " ", "y", "o", "u", "?"] // Added missing space after "are"

function typeText() {
  var i = 0;
  var interval = setInterval(function() {
    var parag = document.getElementById("theParagraph");
    var paragOldText = parag.textContent; // Replaced "parag.innerText" with "parag.textContent"
    parag.textContent = paragOldText + text[i]; // Did it again.
    i++;
    if (text.length == i)
      clearInterval(interval);
  }, 200)
}
<body>
  <p id="theParagraph"></p>
  <button id="typeButton" onclick="typeText()" style="padding:15px">Start typing the sentence</button>
</body>

Also, please note that Konrad Rudolph and bergi answered the why in comments directly on the question.

Zack Plauché
  • 3,307
  • 4
  • 18
  • 34
-3

This problem is a great candidate for an MVC pattern. I discuss this exact problem in my blog. I've provided an MVC for this problem below. (Please excuse the shameless self-promotion.)

const Model = function(){
   const self = this;
   self.index = 0;
   self.text = ["H", "e", "l", "l", "o", " ", "h", "o", "w", " ", "a", "r", "e", " ", "y", "o", "u", "?"];
   self.textString = "",
   self.accumulate = function(){
     const length = self.text.length;
     self.textString = self.textString + self.text[self.index];
     self.index = ++self.index % length;
   }
 }
  const Controller = function(model, elem, milsec){
   const self = this;
   self.elem = elem;
   self.start = function(){
     const interval = setInterval( function(){
      if(model.index===model.text.length-1){
       clearInterval(interval);
     }
       model.accumulate();
       self.elem.innerText = model.textString;   
     }, milsec);
   }
 }
 
 const typeText = function(){
   const model = new Model();
   const theParagraph = document.getElementById('theParagraph');
   const controller = new Controller(model, theParagraph, 200);
   controller.start();
 }
<body>
    <p id="theParagraph"></p>
    <button id="typeButton" onclick="typeText()" style="padding:15px">Start typing the sentence</button>

<p>
  I invite you to go to my <a target='_top' href="https://www.monilito.com/blog/Never-Use-Presentational-Structures-to-Store-State">blog article</a> for an interesting take on this problem.
</p>
</body>
  • This solution unnecessarily complicates the code for no added value, trying to fit a large square peg into a small round hole. – d4nyll Jun 10 '21 at 20:43